용어정리
https://github.com/Java-Bom/ReadingRecord/issues/187
[3.5 ~ 3.7] 용어정리 · Issue #187 · Java-Bom/ReadingRecord
템플릿/콜백 패턴 : c804656(순수자바) a17545c / 6ee0409 (spring jdbc)
github.com
템플릿 콜백 패턴
변하지 않는 작업흐름(템플릿)과 실행되는 것을 목적으로 전달되는 변하는 오브젝트(콜백)으로 나누어 중복코드를 제거하여 재사용성을 높일 수 있는 패턴
ex) 토비의 스프링 발췌 - Calculator
파일에서 텍스트를 한줄씩 읽어들여 계산하는 예제.
여기서 변하는 부분은 계산의 방식(콜백), 변하지 않는 부분은 파일을 읽어들이고 리소스를 닫는 부분이다.(템플릿)
/**
* 템플릿: Calculator 에서 행해지는 연산들에 공통으로 필요한 코드를 템플릿으로 정의한다.
* 여기서는 InputStream 에서 한줄씩 읽으며 try-catch-finally 로 묶인 일련의 과정이 속한다.
*/
public static Integer lineReadTemplate(InputStream in, LineCallback lineCallback, int initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(in));
Integer res = initVal;
String line;
while ((line = br.readLine()) != null) {
res = lineCallback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (Objects.nonNull(br)) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
콜백오브젝트(LineCallback)는 인터페이스로 분리하여 클라이언트에서 구현하도록 한다.
public Integer calcSum(InputStream in) throws IOException {
/*
* 콜백: 메서드에 전달되는 오브젝트. 파라미터지만 값이아니라 특정로직이다.(펑셔널 오브젝트)
* 변하는 부분을 콜백으로 전달한다.
* - line, value는 컨텍스트 정보를 전달하는 매개변수이다. 콜백은 이 정보들을 참조하여 템플릿에 전달된다.
*/
LineCallback sumCallBack = (line, value) -> value + Integer.parseInt(line);
return StreamReaderTemplate.lineReadTemplate(in, sumCallBack, 0);
}
public Integer calcMultiply(InputStream in) throws IOException {
return StreamReaderTemplate.lineReadTemplate(in, (line, value) -> value * Integer.parseInt(line), 1);
}
네거티브 테스트
예외상황에 대한 테스트
@DisplayName("유저 저장테스트")
@Test
void add() {
/*
* 네거티브 테스트
* 예외상항에 대한 테스트
*/
tobyUserDao.add(null); // 의도적으로 null 을 넣는다
tobyUserDao.get("unknown"); // 의도적으로 없는 데이터를 조회한다.
}
자바봄 이슈
Q. 익명클래스의 메모리 할당시점
https://github.com/Java-Bom/ReadingRecord/issues/189
위의 계산기 템플릿/콜백 패턴의 calcMultiply의 (line, value) -> value * Integer.parseInt(line) 익명클래스가 언제 할당되는지 디버깅을 통해 확인
public Integer calcMultiply(InputStream in) throws IOException {
return StreamReaderTemplate.lineReadTemplate(in, (line, value) -> value * Integer.parseInt(line), 1);
}
lineReadTemplate 메서드 진입 시 할당 및 로직 끝날 때 해제
Q. 템플릿/콜백 패턴으로 구현된 스프링 jdbc 의 JdbcTemplate 정리
https://github.com/Java-Bom/ReadingRecord/issues/190
[3.5 ~ 3.7] JdbcTemplate · Issue #190 · Java-Bom/ReadingRecord
p.[페이지] 3.6~3.7 질문 : jdbcTemplate에 대한 예제로 update~query까지 template/callback 패턴이 어떤식으로 적용되었는지 정리해주면 좋을듯. 나중에 이 패턴들 보고 같이 얘기해보면 구현 수준도 많이 높
github.com
update메서드
(PreparedStatementCreator를 직접 생성해서 넣을 수도 있음. 다양한 매개변수 타입을 받을 수 있도록 오버로딩 되어있음)
jdbcTemplate.update("INSERT INTO TOBY_USER VALUES (?, ?, ?)", user.getId(), user.getName(), user.getPassword());
위 update메서드는 첫번째 sql 문을 PreparedStatemetCreator로 변환하여 아래 protected update 메서드를 수행한다.
여기서 execute 의 두번째 인자를 콜백 메서드로 볼 수 있다.
protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss)
throws DataAccessException {
logger.debug("Executing prepared SQL update");
return updateCount(execute(psc, ps -> {
try {
if (pss != null) {
pss.setValues(ps); // 주어진 PreparedStatement(ps)에 값을 세팅한다.
}
int rows = ps.executeUpdate(); // statement 수행
if (logger.isTraceEnabled()) {
logger.trace("SQL update affected " + rows + " rows");
}
return rows; // updateRowCount 반환
}
finally {
if (pss instanceof ParameterDisposer) {
((ParameterDisposer) pss).cleanupParameters();
}
}
}));
}
그리고 execute 메서드가 변하지 않는 "템플릿"에 해당한다. 변하는 부분에 해당하는 PreparedStatementCallback 인터페이스는 제네릭을 사용함으로써 반환 타입도 클라이언트가 정할 수 있게 하였다.
T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException;
@Override
@Nullable
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
throws DataAccessException {
Assert.notNull(psc, "PreparedStatementCreator must not be null");
Assert.notNull(action, "Callback object must not be null");
if (logger.isDebugEnabled()) {
String sql = getSql(psc);
logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
}
Connection con = DataSourceUtils.getConnection(obtainDataSource());
PreparedStatement ps = null;
try {
ps = psc.createPreparedStatement(con);
applyStatementSettings(ps);
T result = action.doInPreparedStatement(ps);
handleWarnings(ps);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
if (psc instanceof ParameterDisposer) {
((ParameterDisposer) psc).cleanupParameters();
}
String sql = getSql(psc);
psc = null;
JdbcUtils.closeStatement(ps);
ps = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("PreparedStatementCallback", sql, ex);
}
finally {
if (psc instanceof ParameterDisposer) {
((ParameterDisposer) psc).cleanupParameters();
}
JdbcUtils.closeStatement(ps);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
execute 메서드를 보면 아래의 부분을 통해 콜백을 실행하고 나머지 부분은 변하지 않는 템플릿에 해당하는 것을 확인 할 수 있다. 즉, 모든 jdbc sql 실행 시 수행되어야 하는 검증(Assert문), Connection 연결, PreparedStatement생성, 예외처리, 자원 반환(finally) 을 재사용할 수 있도록 템플릿화 하였다.
T result = action.doInPreparedStatement(ps);
이 execute 템플릿은 한번 더 감싸서 batchUpdate 에서도 사용하고 query 메서드에도 사용된다.
public <T> T query(
PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
throws DataAccessException {
Assert.notNull(rse, "ResultSetExtractor must not be null");
logger.debug("Executing prepared SQL query");
return execute(psc, new PreparedStatementCallback<T>() {
@Override
@Nullable
public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
ResultSet rs = null;
try {
if (pss != null) {
pss.setValues(ps);
}
rs = ps.executeQuery();
return rse.extractData(rs); // resultData 반환, update와 다른점
}
finally {
JdbcUtils.closeResultSet(rs);
if (pss instanceof ParameterDisposer) {
((ParameterDisposer) pss).cleanupParameters();
}
}
}
});
}
아래 query 메서드는 내부 클래스로 callback 을 정의하고 execute 에 전달했다. 제네릭을 사용했기 때문에 해당 메서드에 ResultSetExtractor 파라미터의 제네릭 타입으로 타입을 결정하기 때문에 콜백 클래스를 미리 생성하여 재사용하지 못하고 실행될 때마다 메서드 내부에서 생성하는 것을 볼 수 있다.
@Override
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
/**
* Callback to execute the query.
*/
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
@Nullable
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
return rse.extractData(rs);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
'Reading Record > 토비의 스프링 3.0' 카테고리의 다른 글
[토비의 스프링 5장] 5.2 서비스 추상화 (0) | 2021.01.21 |
---|---|
1.8 ~ 2.2 (0) | 2020.11.27 |
[토비의 스프링] [1장] 오브젝트와 의존관계 1.4 ~ 1.7 (0) | 2020.11.05 |