728x90

저번 workWithStatementStrategy로 StatementStrategy 구현체를 넘기는 과정에서 ide가 자동으로 콜백을 추천했었다.

callback은 실행되는 것을 목적으로 다른 오브젝트의 메서드에 넘겨주는 오브젝트를 말한다.

안에 무언가 정보는 신경쓰지 않고, 그 오브젝트의 메서드를 실행하기 위해서이다.

 

인터페이스가 여러 개의 메서드를 가지는 전략 패턴과 달리, 콜백은 단일 메서드만을 가지는 인터페이스에서 시작한다.

어차피 그 메서드 하나만 실행되기 때문이다.

 

저번에 만들었던 workWithStatementStrategy()에 콜백으로 넘겨보자.

workWithStatementStrategy()에 StatementStrategy를 넘겨주어야 하기 때문에

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

 

해당 인터페이스의 구현체를 만들어 넘겨줬었고, 그럼 또 makePreparedStatement에 Connection을 넘겨줘야 했었다.

 

하지만 callback을 이용하면 해당 구현체를 만들지 않고 사용 할 수 있다.

    public void add(User user) throws SQLException {
        this.jdbcContext.workWithStatementStrategy(c -> {
            PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");

            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());

            return ps;
        });
    }

 

이렇게 람다식으로 connection을 이용해 preparedStatement를 만드는 과정만 넘겨주면 된다.

 

콜백을 작업 흐름은 다음과 같다고 한다.

나는 그냥 파라미터로 함수를 넘긴다고 생각하려고 한다.

그리고 호출된 메서드에서 해당 함수로 작업을 수행한다고 이해했다.

 

이런 callback 패턴은 try-catch-finally에도 굉장히 많이 사용한다고 한다.

예제를 위해 테스트 코드를 작성해보자.

package seungkyu;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Objects;

public class CalcSumTest {

    static class Calculator{

        public Integer calcSum(String filePath) throws IOException {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
            int sum = 0;
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                sum += Integer.parseInt(line);
            }

            bufferedReader.close();
            return sum;
        }
    }

    @Test
    public void sumOfNumbers() throws IOException {
        ClassLoader classLoader = getClass().getClassLoader();
        Calculator calculator = new Calculator();
        int sum = calculator.calcSum(Objects.requireNonNull(classLoader.getResource("numbers.txt")).getFile());
        Assertions.assertTrue(sum > 0);
    }
}

 

그냥 단순하게 파일 읽어서 값 테스트하는 코드이다.

이제 throws한 IOException을 잡아보자.

 

public class CalcSumTest {

    static class Calculator{

        public Integer calcSum(String filePath) {
            try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
                int sum = 0;
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    sum += Integer.parseInt(line);
                }

                bufferedReader.close();
                return sum;
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
            return 1;
        }
    }

    @Test
    public void sumOfNumbers() {
        ClassLoader classLoader = getClass().getClassLoader();
        Calculator calculator = new Calculator();
        int sum = calculator.calcSum(Objects.requireNonNull(classLoader.getResource("numbers.txt")).getFile());
        Assertions.assertTrue(sum > 0);
    }
}

 

일단 try-catch-finally로 핸들링 하도록 했다.

 

하지만 메서드들이 추가될 때마다 예외처리 코드를 복사할 수는 없을 것이다.

이제 이 중복이 될 코드들을 템플릿/콜백으로 제거해보도록 하자.

 

가장 간단하게 생각이 드는 것은 템플릿이 파일을 열고 각 라인을 읽어올 수 있는 BufferedReader를 만들어서 콜백에게 전달해주고, 콜백이 각 라인을 읽어서 알아서 처리한 후 결과를 돌려주는 것이다.

 

일단 파일을 읽어서 결과를 반환해주는 메서드를 만들어준다.

    public int fileReadTemplate(String filePath, BufferedReaderCallback bufferedReaderCallback){
            try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
                return bufferedReaderCallback.doSomethingWithBufferedReader(bufferedReader);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

parameter로는 파일의 위치와 어떤 작업을 수행 할건지에 대한 callback을 작성해준다.

 

넘겨주는 callback의 interface도 만들어준다.

public interface BufferedReaderCallback {

    int doSomethingWithBufferedReader(BufferedReader bufferedReader) throws IOException;
}

 

이제 더하기에 대한 연산을 만들어보자.

    public Integer calcSum(String filePath) {
            BufferedReaderCallback sumCallback = bufferedReader -> {
                int sum = 0;
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    sum += Integer.parseInt(line);
                }
                return sum;
            };

            return fileReadTemplate(filePath, sumCallback);
        }

BufferedReaderCallback의 구현체를 lambda로 만들어서 바로 fileReadTemplate의 결과를 반환해준다.

 

이런 식으로 작성하면 일단 try-catch-finally에 대한 중복은 제거할 수 있다.

 

더 간결하게 만들 수 있을까?

만약 여기 곱하기가 추가된다고 해보자.

그러면 while문으로 버퍼를 읽는 부분도 중복이 될거고, 그 부분도 추출하고 싶어진다.

 

그러기 위해서는 또 callback을 위한 인터페이스를 만들고

public interface LineCallback {
    int doSomethingWithLine(String line, int value);
}

 

해당 인터페이스를 이용하는 메서드도 만든다. (fileReadTemplate메서드 처럼)

 

    public int lineReadTemplate(String filePath, LineCallback callback, int initVal){
            BufferedReader bufferedReader;
            try{
                bufferedReader = new BufferedReader(new FileReader(filePath));
                int res = initVal;
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    res = callback.doSomethingWithLine(line, res);
                }
                return res;
            }
            catch (IOException e){
                System.out.println(e.getMessage());
            }
            return initVal;
        }

 

메서드를 읽어보자면, 파일을 읽어오기 위한 경로와 작업을 수행할 메서드 그리고 초기값을 받아온다.

 

res 값을 callback 메서드로 계속 변경하기 때문에 더하기, 곱하기에 대한 메서드만 넣어주면 된다.

    public Integer calcSum(String filePath) {
            return lineReadTemplate(filePath, (line, value) -> {
                return Integer.parseInt(line) + value;
            }, 0);
        }

덕분에 calcSum 메서드도 더하기에 대한 연산만 넣어주면 끝나게 된다.

 

그리고 만약 곱하기에 대한 메서드도 필요하다면 

    public Integer calcMulti(String filePath) {
            return lineReadTemplate(filePath, (line, value) -> {
                return Integer.parseInt(line) * value;
            }, 1);
        }

이렇게 간단하게 곱하기에 대한 얀산만 넣어준다.

 

calcSum, calcMulti를 보면 중복되는 부분도 없이 리펙토링 된 것을 볼 수 있다.

 

굉장히 좋은 방법이지만, 솔직히 실전에서는 callback을 쓸 생각이 잘 안난다.

프론트 개발자들은 자주 사용하던데, 나도 좀 적극적으로 사용하려고 노력해야겠다.

+ Recent posts