템플릿/콜백 패턴
- 전략 패턴 : 일정한 패턴을 갖는 작업 흐름이 존재하고 그 중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합
- 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
템플릿
고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 것
콜백
실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
3.5.1 템플릿/콜백의 동작원리
템플릿/콜백의 특징
- 보통 단일 메소드 인터페이스를 사용함
- 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적
- 메소드에 보통 파라미터가 존재
- 템플릿 작업 흐름 중 만들어지는 컨텍스트 정보를 전달받을 때 사용됨
템플릿/콜백의 일반적인 작업 흐름
- 클라이언트가 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공
- 만들어진 콜백은 클라이언트가 템플릿 메소드를 호출할 때 파라미터로 전달됨
- 템플릿은 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트 메소드를 호출함
- 콜백은 참조정보를 이용하여 작업을 수행하고 그 결과를 다시 템플릿에 돌려줌
- 템플릿은 돌려받은 정보를 사용하여 작업을 마저 수행함
- 경우에 따라 최종 결과를 클라이언트에게 다시 돌려주기도 함
JdbcContext에 적용된 템플릿/콜백
- UserDao.add()가 익명 Callback 오브젝트를 생성한다.
- Callback 오브젝트는 workWithStatementStrategy()를 호출할 때 파라미터로 전달된다.
- 템플릿은 작업을 하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트 메소드(StatementStrategy)를 호출한다.
- 콜백은 참조정보를 이용하여 작업을 수행하고 그 결과(PreparedStatement)를 다시 템플릿에 돌려준다.
- 템플릿은 돌려받은 정보를 사용하여 작업을 마저 수행한다.
- void 형이므로 최종 결과는 돌려주지 않는다.
3.5.2 편리한 콜백의 재활용
템플릿/콜백의 장점과 단점
- 장점 : DAO 메소드는 간결해지고, 최소한의 데이터 액세스 로직만 갖게 된다.
- 단점 : 매번 익명 내부 클래스를 사용할 경우 코드를 작성하고 읽기가 조금 불편해진다.
콜백의 분리와 재활용
JDBC try/catch/finally에 적용했던 방법을 현재 UserDao에 적용
- 콜백 오브젝트 코드는 고정된 SQL 쿼리 하나를 담아서 PreparedStatement를 만든다.
- deleteAll()과 유사한 내용의 콜백 오브젝트가 반복될 가능성이 높다.
- deleteAll()에서 바뀔 수 있는 내용은 "delete from users"라는 문자열 뿐
- SQL 문장을 파라미터로 받아 바꿀 수 있도록 하고, 메소드 내용 전체를 분리하여 별도의 메소드로 구성
public class UserDao {
// ...
public void deleteAll() throws SQLException {
executeSql("DELETE FROM users WHERE 1=1");
}
// ...
private void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement(query);
return ps;
}
}
);
}
}
콜백과 템플릿의 결합
excuteSql() 메소드는 UserDao 뿐만 아니라 다양한 DAO 클래스에서 활용이 가능하다.
- DAO가 공유할 수 있는 템플릿 클래스(JdbcContext) 안으로 옮길 수 있다.
JdbcContext.java
- public으로 수정
public class JdbcContext {
// ...
public void executeSql(final String query) throws SQLException {
workWithStatementStrategy(
new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement(query);
return ps;
}
}
);
}
}
UserDao.java
public class UserDao {
public void deleteAll() throws SQLException {
this.jdbcContext.executeSql("DELETE FROM users WHERE 1=1");
}
}
모든 DAO메소드에서 excuteSql() 메소드 사용가능
- JdbcContext 안에 클라이언트와 템플릿, 콜백이 함께 공존하면서 동작하는 구조
일반적으로 성격이 다른 코드는 가능한 분리하는 편이 나음
- 이 경우에는 하나의 목적을 갖고 서로 긴밀하게 연관
- 응집력이 강한 코드들이므로 한 군데 모여있는 것이 유리
- add() 메소드에도 사용가능 - 가변인자(varargs) 사용해야 함
3.5.3 템플릿/콜백의 응용
템플릿/콜백 패턴은 스프링에서만 사용할 수 있는 독점적인 기술은 아니지만 스프링은 이 패턴을 적극적으로 활용한다.
- 스프링 개발자라면 스프링의 템플릿/콜백 기능을 사용할 줄 알아야하며, 필요한 경우 직접 만들어서 사용할 줄 알아야 한다.
자주 반복되는 코드
- 메소드 추출
- 가장 간단
- 전략 패턴의 적용
- 일부 작업을 필요에 따라 바꿔서 사용해야 할 경우
- 템플릿/콜백의 적용
- 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 가능성이 있는 경우
테스트와 try/catch/finally
모든 라인의 숫자를 더한 합을 돌려주는 코드
CalcSumTest.java
public class CalcSumTest {
@Test
public void sumOfNumbers() throws IOException {
String path = getClass().getResource("").getPath() + "numbers.txt";
Calculator calculator = new Calculator();
int sum = calculator.calcSum(path);
assertThat(sum, is(10));
}
}
Calculator.java
public class Calculator {
public int calcSum(String path) throws IOException {
BufferedReader br = null;
Integer sum = 0;
String line = null;
br = new BufferedReader(new FileReader(path));
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
br.close
return sum;
}
}
Calculator에서 예외 발생 시 br이 닫히지 않고 메소드를 빠져나가버림
- calcSum()에 예외처리 추가
public class Calculator {
public int calcSum(String path) throws IOException {
BufferedReader br = null;
Integer sum = 0;
String line = null;
try {
br = new BufferedReader(new FileReader(path));
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sum;
}
}
중복의 제거와 템플릿/콜백 설계
곱을 계산하는 기능을 추가하여야 하며, 이후 숫자들을 여러가지 방식으로 처리하는 기능이 계속 추가된다고 함. (템플릿/콜백의 적용)
- 템플릿이 파일을 열고
- 각 라인을 읽는 BufferedReader를 만들어 콜백에게 전달
- 콜백은 각 라인을 읽어서 처리
- 최종 결과를 템플릿에게 돌려줌
BufferedReaderCallback.java
public interface BufferedReaderCallback {
Integer doSomethingWithReader(BufferedReader br) throws IOException;
}
Calculator.java
public class Calculator {
public int 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 fileReadTemplate(String path, BufferedReaderCallback callback) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
int ret = callback.doSomethingWithReader(br);
return ret;
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
CalcSumTest.java
public class CalcSumTest {
Calculator calculator;
String numFilePath;
@Test
public void sumOfNumbers() throws IOException {
this.calculator = new Calculator();
this.numFilePath = getClass().getResource("").getPath() + "numbers.txt";
assertThat(calculator.calcSum(this.numFilePath), is(10));
}
}
곱하기 메서드 추가
CalcSumTest.java
public class CalcSumTest {
Calculator calculator;
String numFilePath;
@Before
public void setUp() {
this.calculator = new Calculator();
this.numFilePath = getClass().getResource("").getPath() + "numbers.txt";
}
@Test
public void sumOfNumbers() throws IOException {
assertThat(calculator.calcSum(this.numFilePath), is(10));
}
@Test
public void multipleOfNumbers() throws IOException {
assertThat(calculator.calcMultiple(this.numFilePath), is(24));
}
}
Calculator.java
public Integer calcMultiple(String filepath) throws IOException {
BufferedReaderCallback sumCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multifly = 1;
String line = null;
while ((line = br.readLine()) != null) {
multifly *= Integer.valueOf(line);
}
return multifly;
}
};
return fileReadTemplate(filepath, sumCallback);
}
}
템플릿/콜백의 재설계
calcSum()과 calcMultiply()의 콜백에 공통적인 패턴이 발견됨
- 라인별 작업을 콜백 인터페이스로 정의할 수 있음
LineCallBack.java
public interface LineCallback {
Integer doSomethingWithLine(String line, Integer value);
}
LineCallback을 사용하는 템플릿
public class Calculator {
public Integer lineReadTemplate(String path, LineCallback callback, int initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
Integer res = initVal;
String line = null;
while ((line = br.readLine()) != null) {
res = callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
템플릿에 각 라인을 읽는 작업이 추가
- while 루프 안에서 콜백을 호출
Calculator.java
public class Calculator {
public int 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 calcMultiple(String filepath) throws IOException {
LineCallback multipleCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) {
return value * Integer.valueOf(line);
}
};
return lineReadTemplate(filepath, multipleCallback, 1);
}
public Integer lineReadTemplate(String path, LineCallback callback, int initVal) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
Integer res = initVal;
String line = null;
while ((line = br.readLine()) != null) {
res = callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
파일 처리 코드는 템플릿으로 분리되고, 순수한 계산 로직만 남아 있기 때문에 코드의 관심이 무엇인지 명확하게 보인다.
제네릭스를 이용한 콜백 인터페이스
LineCallback()과 lineReadTemplate()은 리턴타입이 Integer로 고정되어 있음
- 결과 타입을 다양하게 가져가려면 제네릭스를 이용하면 됨
- LineCallback의 리턴 값과 파라미터 타입을 제네릭 타입 파라미터 T로 선언
LineCallback.java
public interface LineCallback<T> {
T doSomethingWithLine(String line, T value);
}
Calculator.java
public class Calculator {
public int calcSum(String filepath) throws IOException {
LineCallback<Integer> sumCallback = new LineCallback<Integer>() {
// ...
};
return lineReadTemplate(filepath, sumCallback, 0);
}
public Integer calcMultiple(String filepath) throws IOException {
LineCallback<Integer> multipleCallback = new LineCallback<Integer>() {
// ...
};
return lineReadTemplate(filepath, multipleCallback, 1);
}
public <T> T lineReadTemplate(String path, LineCallback<T> callback, T initVal) throws IOException {
// ...
}
}
Concatenate 메서드 추가
Calculator.java
public String concatenate(String filepath) throws IOException {
LineCallback<String> concatCallback = new LineCallback<String>() {
@Override
public String doSomethingWithLine(String line, String value) {
return value + line;
}
};
return lineReadTemplate(filepath, concatCallback, "");
}
Test.java
@Test
public void concatenateNumbers() throws IOException {
assertThat(calculator.concatenate(this.numFilePath), is("1234"));
}
' Spring > 토비의 스프링 3.1' 카테고리의 다른 글
3.7 정리 (0) | 2019.01.23 |
---|---|
3.6 스프링의 JdbcTemplate (0) | 2019.01.23 |
3.4 컨텍스트와 DI (0) | 2019.01.22 |
3.3 JDBC 전략 패턴의 최적화 (0) | 2019.01.18 |
3.2 변하는 것과 변하지 않는 것 (0) | 2019.01.18 |