지금까지 UserDao와 StatementStrategy, JdbcContext를 이용해 만든 코드는 일종의 전략 패턴이 적용된 것이라고 볼 수 있다. 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다.
전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.
-템플릿/콜백의 동작원리
템플릿 : 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름
콜백 : 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트
템플릿/콜백의 특징
콜백은 보통 단일 메소드 인터페이스를 사용한다.
하나의 템플릿에서 여러 가지 종류의 전략을 사용해야 한다면 하나 이상의 콜백 오브젝트를 사용할 수 있다.
콜백은 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면 된다.
콜백 인터페이스의 메소드에는 보통 파라미터가 있다.
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.
- 템플릿은 정해진 작업 흐름을 따라 진행하다가 내부에서 생성한 참조 정보를 가지고 콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조 정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
- 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.
- 편리한 콜백의 재활용
콜백의 분리와 재활용
복잡한 익명 내부 클래스의 사용을 최소화할 수 있는 방법을 찾아보자. 중복될 가능성이 있는 바뀌지 않는 부분을 분리해보자.
별도의 메소드를 만들어서 분리해보면 아래와 같다.
이렇게 해서 재활용 가능한 콜백을 담은 메소드가 만들어졌다. 처음 deleteAll()메소드와 지금 deleteAll() 메소드를 비교해보면 차이점을 느낄 수 있다.
콜백과 템플릿의 결합
더 나아가서 executeSql() 메소드를 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다.
JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql() 메소드를 옮긴다고 해도 문제 될 것은 없다.
메소드가 이동을 했으니 UserDao의 메소드에서도 jdbcContext를 통해 executeSql()을 호출하도록 수정하자.
이제 모든 DAO 메소드에서 executeSql() 메소드를 사용할 수 있게 됐다.
- 템플릿/콜백의 응용
여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자. 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다.
그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하도록 만든다. 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴을 적용할 수 있다.
가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다. 이런 코드가 한두 번 사용되는 것이 아니라 자주 반복된다면 템플릿/콜백 패턴을 적용하기 적당하다.
테스트와 try/catch/finally
예제 하나를 만들어보기 위해 네 개의 숫자를 담고 있는 numbers.txt 파일을 하나 준비하자. 아래와 같은 테스트를 만들자.
Calculator 클래스도 아래와 같이 만들어주자.
calSum() 메소드에 try/catch/finally를 모두 적용하면 아래와 같은 코드가 만들어진다.
DAO의 JDBC코드에 적용했던 것과 기본 개념은 같다.
중복의 제거와 템플릿/콜백 설계
모든 숫자의 곱을 계산하는 기능을 추가한다는 요구가 발생했을 때 템플릿/콜백 패턴을 적용해보자.
인터페이스를 하나 만들어주고 템플릿 부분을 메소드로 분리해보자.
BufferedReader를 만들어서 넘겨주는 것과 그 외의 모든 번거로운 작업에 대한 작업 흐름은 템플릿에서 진행하고 준비된 BufferedReader를 이용해 작업을 수행하는 부분은 콜백으로 호출해서 처리하도록 만들었다.
테스드 코드에서도 자주 반복되는 코드를 @Before에 넣어서 코드를 정리해주었다.
곱하기를 구현한 calMultiply() 메소드도 각 라인의 숫자를 더하는 대신에 곱하는 기능을 담아서 콜백을 사용하도록 만들어 주었다.
그 부분을 제외하면 calSum()과 코드 자체는 유사하다.
- 스프링의 Jdbctemplate
템플릿과 콜백의 기본적인 원리와 동작 방식, 만드는 방법을 알아봤으니 이번에는 스프링이 제공하는 템플릿/콜백 기술을 살펴보자.
이전에 만든 JdbcContext는 버리고 스프링의 JdbcTemplate로 바꿔보자.
JdbcTemplate는 생성자의 파라미터로 DataSource를 주입하면 된다. 이제 템플릿을 사용할 준비가 됐다.
update()
PreparedStatementCreator 타입의 콜백을 받아서 사용하는 JdbcTemplate의 템플릿 메소드는 update()다.
아래는 JdbcTemplate의 콜백과 템플릿 메소드를 사용하도록 수정한 deleteAll()메소드이다.
JdbcTemplate에서 SQL 문장만 전달하면 미리 준비된 콜백을 만들어서 템플릿을 호출하는 편리한 메소드가 있다.
JdbcTemplate의 내장 콜백을 사용하는 메소드를 호출해서 deleteAll() 메소드를 수정해보겠다.
JdbcTemplate는 앞에서 구상만 해보고 만들지 못했던 add() 메소드에 대한 편리한 메소드도 제공한다.
치환자(?)를 가진 SQL로 PreparedStatement를 만들고 함께 제공하는 파라미터를 순서대로 바인딩해주는 기능을 가진 update()메소드를 사용할 수 있다. SQL과 함께 가변 인자로 선언된 파라미터를 제공해주면 된다.
this.jdbcTemplate.update("insert into users (id,name,password) values(?,?,?)",user.getId(),user.getName(),user.getPassword());
- queryForInt()
getCount()는 SQL 쿼리를 실행하고 ResultSet을 통해 결과 값을 가져오는 코드다. 이런 작업 흐름을 가진 코드에서 사용할 수 있는 템플릿은 PreparedStatementCreator콜백과 ResultSetExtractor콜백을 파라미터로 받는 query() 메소드다.
첫 번째 PreparedStatementCreator콜백은 템플릿으로부터 Connection을 받고 PreparedStatement를 돌려준다.
두 번째 ResultSetExtractor는 템플릿으로부터 ResultSet을 받고 거기서 추출한 결과를 돌려준다.
이런 기능을 가진 콜백을 내장하고 있는 queryForInt()라는 편리한 메소드를 제공한다. 위처럼 복잡한 이중 콜백을 아래와 같이 한 줄로 바꿀 수 있다.
- queryForObject()
get() 메소드는 앞에서 한 작업과 달리 ResultSet에 getCount()처럼 단순한 값이 아니라 복잡한 UserVO 오브젝트를 만드는 작업이 필요하다. ResultSet의 결과를 UserVO 오브젝트를 만들어 프로퍼티에 넣어줘야 한다.
이를 위해 RowMapper 콜백을 사용하겠다. RowMapper는 ResultSet의 로우 하나를 매핑하기 위해 사용되기 때문에 여러 번 호출될 수 있다는 점이다. queryForObject()를 적용한 코드를 살펴보자.
RowMapper에서는 현재 ResultSet이 가리키고 있는 로우의 내용을 UserVO 오브젝트에 그대로 담아서 리턴해주기만 하면 된다. RowMapper가 리턴한 UserVO 오브젝트는 queryForObject() 메소드의 리턴 값으로 get() 메소드에 전달된다.
- getAll() 구현
query() 메소드를 사용해서 getAll()메소드를 만들어보자. query()는 쿼리의 결과가 로우 하나일 때 사용하고, 여러 개의 로우가 결과로 나오는 일반적인 경우에도 사용할 수 있다. query()의 리턴 타입은 List<T>이다. query()는 제네릭 메소드로 타입은 파라미터로 넘기는 RowMapper<T> 콜백 오브젝트에서 결정된다.
첫 번째 파라미터에는 실행할 SQL 쿼리를 넣는다. 바인딩할 파라미터가 있다면 두 번째 파라미터에 추가할 수도 있다.
마지막 파라미터는 RowMapper 콜백이다. RowMapper는 현재 로우의 내용을 UserVO 타입 오브젝트에 매핑해서 돌려준다. 이렇게 만들어진 UserVO 오브젝트는 템플릿이 미리 준비한 List<UserVO> 컬렉션에 추가된다. 모든 로우에 대한 작업이 마치면 모든 로우에 대한 UserVO 오브젝트를 담고 있는 List<UserVO> 오브젝트가 리턴된다.
- 재사용 가능한 콜백의 분리
DI를 위한 코드 정리
이제 필요 없어진 DataSource 인스턴스 변수를 제거하자. UserDao의 모든 메소드가 JdbcTemplate을 이용하도록 만들었으니 DataSource를 직접 사용할 일은 없다.
중복 제거
get()과 getAll() 메소드를 보면 사용한 RowMapper의 내용이 똑같다는 사실을 알 수 있다. 아래와 같이 userMapper라는 이름으로 인스턴스 변수를 만들고 사용할 매핑용 콜백 오브젝트를 초기화하도록 만든다. 익명 내부 클래스는 클래스 안에서라면 어디서든 만들 수 있다.
위처럼 만든 userMapper는 아래와 같이 사용하면 된다.
'Spring > 토비의 스프링 정리' 카테고리의 다른 글
5장 서비스 추상화 (0) | 2022.04.14 |
---|---|
3.3 JDBC 전략 패턴의 최적화, 3.4 컨텍스트와 DI (0) | 2022.04.08 |
3.1 다시 보는 초난감 DAO (0) | 2022.04.07 |
2.5 학습 테스트로 배우는 스프링 (0) | 2022.04.06 |
2.4 스프링 테스트 적용 (0) | 2022.04.05 |