본문 바로가기

Spring/토비의 스프링 3.1

3.5 템플릿과 콜백


템플릿/콜백 패턴

  • 전략 패턴 : 일정한 패턴을 갖는 작업 흐름이 존재하고 그 중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합
  • 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

템플릿
고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 것
콜백
실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트

3.5.1 템플릿/콜백의 동작원리

템플릿/콜백의 특징

  1. 보통 단일 메소드 인터페이스를 사용함
    • 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적
  2. 메소드에 보통 파라미터가 존재
    • 템플릿 작업 흐름 중 만들어지는 컨텍스트 정보를 전달받을 때 사용됨

템플릿/콜백의 일반적인 작업 흐름

  1. 클라이언트가 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공
  2. 만들어진 콜백은 클라이언트가 템플릿 메소드를 호출할 때 파라미터로 전달됨
  3. 템플릿은 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트 메소드를 호출함
  4. 콜백은 참조정보를 이용하여 작업을 수행하고 그 결과를 다시 템플릿에 돌려줌
  5. 템플릿은 돌려받은 정보를 사용하여 작업을 마저 수행함
  6. 경우에 따라 최종 결과를 클라이언트에게 다시 돌려주기도 함

JdbcContext에 적용된 템플릿/콜백

  1. UserDao.add()가 익명 Callback 오브젝트를 생성한다.
  2. Callback 오브젝트는 workWithStatementStrategy()를 호출할 때 파라미터로 전달된다.
  3. 템플릿은 작업을 하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트 메소드(StatementStrategy)를 호출한다.
  4. 콜백은 참조정보를 이용하여 작업을 수행하고 그 결과(PreparedStatement)를 다시 템플릿에 돌려준다.
  5. 템플릿은 돌려받은 정보를 사용하여 작업을 마저 수행한다.
  6. 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 템플릿/콜백의 응용

템플릿/콜백 패턴은 스프링에서만 사용할 수 있는 독점적인 기술은 아니지만 스프링은 이 패턴을 적극적으로 활용한다.

  • 스프링 개발자라면 스프링의 템플릿/콜백 기능을 사용할 줄 알아야하며, 필요한 경우 직접 만들어서 사용할 줄 알아야 한다.

자주 반복되는 코드

  1. 메소드 추출
    • 가장 간단
  2. 전략 패턴의 적용
    • 일부 작업을 필요에 따라 바꿔서 사용해야 할 경우
  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;
    }
}

중복의 제거와 템플릿/콜백 설계

곱을 계산하는 기능을 추가하여야 하며, 이후 숫자들을 여러가지 방식으로 처리하는 기능이 계속 추가된다고 함. (템플릿/콜백의 적용)

  1. 템플릿이 파일을 열고
  2. 각 라인을 읽는 BufferedReader를 만들어 콜백에게 전달
  3. 콜백은 각 라인을 읽어서 처리
  4. 최종 결과를 템플릿에게 돌려줌

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