3.5.1 템플릿(template)
- 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
- 고정된 틀 안에 바꿀수 있는 부분을 넣어서 사용하는 경우를 템플릿이라 함
- 변하는 것과 변하지 않는 것을 분리
- 고정된 틀의 로직을 갖는 템플릿 메소드를 슈퍼 클래스에 두고, 바뀌는 부분을 서브 클래스의 메소드에 두는 구조
3.5.2 콜백(Callback)
- 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
- 파라미터로 전달되지만, 값을 참조하는 것이 아닌 특정 로직을 담은 메소드 실행을 목적
- 자바에서는 메소드 자체를 전달하는 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달
- 이를, 펑셔널 오브젝트(Functional Object)라 함
3.5.3 템플릿/콜백
- 템플릿은 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이며, 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트
- 보통, 전략 패턴(템플릿) 및 익명 내부 클래스(콜백)를 같이 사용함
- 여러 개의 메소드를 지닌 일반적 전략 패턴과 달리, 템플릿/콜백은 단일 메소드 인터페이스 사용
- 콜백 인터페이스 메소드의 파라미터는 컨텍스트 정보(DB Connection)를 받을 때 사용
3.5.4 템플릿/콜백 메소드의 작업흐름
- 작업흐름
- 클라이언트의 역할은 템플릿 안에서 콜백 오브젝트(펑셔널 오브젝트)를 만들고, 콜백이 참조할 정보 제공. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달
- 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출. 콜백 작업 수행 후 결과를 템플릿에 전달템플릿은 콜백 작업 수행 결과를 사용해 작업을 마저 수행. 최종 결과를 클라이언트에게 전달해 주기도 함
- 템플릿은 콜백 작업 수행 결과를 사용해 작업을 마저 수행. 최종 결과를 클라이언트에게 전달해 주기도 함
- 클라이언트가 템플릿을 호출하면서 콜백 오브젝트를 전달하는 것은 메소드 레벨의 DI
- 템플릿/콜백 방식은 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달
- 콜백 오브젝트는 (익명)내부 클래스로서, 클라이언트 내 메소드의 멤버를 직접 참조가능
- 클라이언트와 콜백이 강하게 결합된다는 특징이 있음
- 즉, 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스를 사용한 전략과 결합한 독특한 활용법임
3.5.5 JdbcContext에 적용된 템플릿/콜백
- 템플릿과 클라이언트가 메소드 단위인것이 특징임
- 템플릿의 리턴 값이 없음
3.5.6 콜백의 분리와 재활용
- 현재 템플릿/콜백 방식은 상대적으로 코드 작성하고 읽기 불편
- 복잡한 익명 내부 클래스의 사용 최소화
- deleteAll()에서 변하는 것은 SQL 실행 문장뿐임
- 변하지 않는 부분을 executeSql()로 추출하고, JdbcContext에 옮김
- 구체적 구현, 내부의 전략 패턴, 코드에 의한 DI, 익명 내부 클래스 등의 기술은 감추고, 외부에 꼭 필요한 기능만 제공하는 단순 메소드만 노출
- 가변인자(String... str)를 통해 부가 정보를 받음(책에 가변인자 부분 코드가 나와있지 않음)
public class UserDao {
public void add(final User user) throws SQLException {
String id = user.getId();
String name = user.getName();
String password = user.getPassword();
String query = "insert into users(id, name, password) value (?,?,?)";
this.jdbcContext.executeSql(query, id, name, password);
}
public void deleteAll() throws SQLException{
String query = "delete from users";
this.jdbcContext.executeSql(query);
}
}
public class JdbcContext {
public void executeSql(final String query, final String... str) throws SQLException {
workWithStatementStrategy(new StatementStrategy() {
public PreparedStatement makePrepareStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement(query);
for (int i = 0; i < str.length; i++)
ps.setString(i + 1, str[i]);
return ps;
}
});
}
}
3.5.7 템플릿/콜백의 응용
- 템플릿/콜백 패턴은 스프링이 제공하는 독점적인 기술이 아님
- 하지만, 많은 스프링 API나 기능들은 템플릿/콜백 패턴을 적용하고 있음
- 템플릿/콜백 패턴도 DI와 객체지향 설계를 적극적으로 응용한 결과
- 고정된 작업의 흐름을 가지면서, 반복되는 코드가 있으면 분리
- 전형적 템플릿/콜백 패턴의 후보는 try/catch/finally임(코드에서 반복되기 때문)
3.5.8 테스트와 파일 내부 값 계산
- 파일 하나를 열어서 모든 라인의 숫자를 더해주는 코드
public class CalcSumTest {
@Test
public void sumOfNumber() throws IOException {
Calculator calculator = new Calculator();
int sum = calculator.calcSum(getClass().
getResource("numbers.txt").getPath());
assertThat(sum, is(10));
}
}
public class Calculator {
public Integer calcSum(String filePath) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(filePath));
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
br.close();
return sum;
}
}
3.5.9 try/catch/finally 적용
- br을 닫을 때, null아면 예외 발생하므로 null처리 해야함
public class Calculator {
public Integer calcSum(String filePath) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
Integer sum = 0;
String line = null;
while ((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
}
3.5.10 템플릿/콜백 설계
- 곱하기 기능과 같이 기존의 코드와 유사한 기능 추가시 세번 이상 반복되면 코드를 개선해야 함
- 템플릿/콜백을 적용할 때 템플릿이 콜백에게 전달해줄 내부의 정보, 콜백이 템플릿에게 돌려줄 정보, 템플릿 작업 완료후 클라이언트에게 반환해야 하는 정보 등을 고려
- 변하지 않는 것과 변하는 것을 구분
- 변하지 않는 것 : br 및 try/catch/finally 구조
- 변하는 것 : 계산 부분
- 인터페이스를 만들어 콜백 구현하게 함
- 계산 부분을 sum(br)로 추출
- 변하지 않는 것을 fileReaderTemplate(filepath)로 추출
- 콜백을 위해 BufferdReaderCallback 인터페이스 생성
- 인터페이스에 doSomethingWithReader(br) 생성
- fileReaderTemplate(filepath)의 파라미터에 펑셔널 오브젝트(BufferdReaderCallback)를 추가하고, 콜백 메소드를 계산 부분(sum(br))에 대체
- 기존에 있던 calcSum 함수에서 BufferdReaderCallback 을 구현한 sumCallback() 생성 및 리턴으로 fileReaderTemplate(filepath, sumCallback) 으로 템플릿 호출
public interface BufferedReaderCallback {
Integer doSomethingWithReader(BufferedReader br) throws IOException;
}
public class Calculator {
public Integer 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);
}
private Integer fileReadTemplate(String filePath, BufferedReaderCallback callback)
throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
return callback.doSomethingWithReader(br);
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
}
3.5.11 기능 추가
- 곱하기 기능 추가
- 테스트에서 공통 파일을 다루기 때문에 @Before에서 처리
public class CalcSumTest {
Calculator calculator;
String numFilePath;
@Before
public void setUp(){
calculator = new Calculator();
numFilePath = getClass().getResource("numbers.txt").getPath();
}
@Test
public void multiplyOfNumbers() throws IOException {
assertThat(this.calculator.calcMultiply(this.numFilePath),is(24));
}
@Test
public void sumOfNumber() throws IOException {
assertThat(this.calculator.calcSum(this.numFilePath), is(10));
}
}
public class Calculator {
...
public Object calcMultiply(String filePath) throws IOException {
BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multiply = 1;
String line = null;
while ((line = br.readLine()) != null) {
multiply *= Integer.valueOf(line);
}
return multiply;
}
};
return fileReadTemplate(filePath, multiplyCallback);
}
}
3.5.12 템플릿/콜백의 재설계
- 덧셈, 곱셈의 콜백 부분이 상당히 유사함
- 변하지 않는 부분과 변하는 부분을 분류
- 변하지 않는 부분 : 파일을 반복해서 읽는 부분
- 변하는 부분 : 초기 변수, 계산을 하는 부분
- 변하지 않는 부분을 템플릿에 추가
- 기존의 코드를 이름 및 파라미터만 바꾸고 재활용
public interface LineCallback {
Integer doSomethingWithLine(String line, Integer value) throws IOException;
}
public class Calculator {
public Integer calcSum(String filePath) throws IOException {
LineCallback sumCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) throws IOException {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filePath, sumCallback, 0);
}
public Object calcMultiply(String filePath) throws IOException {
LineCallback multiplyCallback = new LineCallback() {
@Override
public Integer doSomethingWithLine(String line, Integer value) throws IOException {
return value * Integer.valueOf(line);
}
};
return lineReadTemplate(filePath, multiplyCallback, 1);
}
private Integer lineReadTemplate(String filePath, LineCallback callback, Integer initValue)
throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
Integer res = initValue;
String line = null;
while ((line = br.readLine()) != null) {
res = callback.doSomethingWithLine(line, res);
}
return res;
} catch (IOException e) {
System.out.println(e.getMessage());
throw e;
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
}
3.5.13 제네릭스를 이용한 인터페이스
- LineCallback과 lineReadTemplate()은 타입이 Integer로 고정되어 있음
- 제네릭스를 이용하면 다양한 오브젝트를 지원하는 인터페이스나 메소드 정의 가능
- 콜백을 정의할 때 사용할 타입 지정
public class CalcSumTest {
...
@Test
public void concatenateStrings() throws IOException {
assertThat(calculator.concatenate(this.numFilePath), is("1234"));
}
}
public interface LineCallback<T> {
T doSomethingWithLine(String line, T value) throws IOException;
}
public class Calculator {
private <T> T lineReadTemplate(String filePath,
LineCallback<T> callback, T initValue)
throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(filePath));
T res = initValue;
String line = null;
...
}
...
}
...
public String concatenate(String filePath) throws IOException {
LineCallback<String> concatenateCallback = new LineCallback<String>() {
@Override
public String doSomethingWithLine(String line, String value) throws IOException {
return value + Integer.valueOf(line);
}
};
return lineReadTemplate(filePath, concatenateCallback, "");
}
}
'토비의 스프링 정리' 카테고리의 다른 글
토비의 스프링 - 3.7 3장 정리 (0) | 2022.09.30 |
---|---|
토비의 스프링 - 3.6 스프링의 JdbcTemplate (0) | 2022.09.30 |
토비의 스프링 - 3.4 컨텍스트와 DI (0) | 2022.09.30 |
토비의 스프링 - 3.3 JDBC 전략 패턴의 최적화 (0) | 2022.09.29 |
토비의 스프링 - 3.2 변하는 것과 변하지 않는 것 (0) | 2022.09.29 |