본문 바로가기

Reading Record/토비의 스프링 3.0

[토비의 스프링 3장] 템플릿 3.5 ~ 3.7

용어정리

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를 직접 생성해서 넣을 수도 있음. 다양한 매개변수 타입을 받을 수 있도록 오버로딩 되어있음)

(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html)

 

 

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