본문 바로가기

토비의 스프링 정리

토비의 스프링 - 3.5 템플릿과 콜백

3.5.1 템플릿(template)

  • 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
  • 고정된 틀 안에 바꿀수 있는 부분을 넣어서 사용하는 경우를 템플릿이라 함
  • 변하는 것변하지 않는 것을 분리
  • 고정된 틀의 로직을 갖는 템플릿 메소드를 슈퍼 클래스에 두고, 바뀌는 부분을 서브 클래스의 메소드에 두는 구조

3.5.2 콜백(Callback)

  • 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
  • 파라미터로 전달되지만, 값을 참조하는 것이 아닌 특정 로직을 담은 메소드 실행을 목적
  • 자바에서는 메소드 자체를 전달하는 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달
  • 이를, 펑셔널 오브젝트(Functional Object)라 함

3.5.3 템플릿/콜백

  • 템플릿은 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이며, 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트
  • 보통, 전략 패턴(템플릿)익명 내부 클래스(콜백)를 같이 사용함
  • 여러 개의 메소드를 지닌 일반적 전략 패턴과 달리, 템플릿/콜백은 단일 메소드 인터페이스 사용
  • 콜백 인터페이스 메소드의 파라미터는 컨텍스트 정보(DB Connection)를 받을 때 사용

3.5.4 템플릿/콜백 메소드의 작업흐름

  • 작업흐름
    1. 클라이언트의 역할은 템플릿 안에서 콜백 오브젝트(펑셔널 오브젝트)를 만들고, 콜백이 참조할 정보 제공. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달
    2. 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출. 콜백 작업 수행 후 결과를 템플릿에 전달템플릿은 콜백 작업 수행 결과를 사용해 작업을 마저 수행. 최종 결과를 클라이언트에게 전달해 주기도 함
    3. 템플릿은 콜백 작업 수행 결과를 사용해 작업을 마저 수행. 최종 결과를 클라이언트에게 전달해 주기도 함
    💡 즉, 전달받은 펑셔널 오브젝트는 템플릿에서 사용될 때 호출됨
  • 클라이언트가 템플릿을 호출하면서 콜백 오브젝트를 전달하는 것은 메소드 레벨의 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, "");
    }
}