템플릿/콜백 패턴 예시, 익명 클래스를 람다식으로 변환

2022. 1. 17. 13:51북리뷰/토비의 봄

728x90

간단한 템플릿/콜백 예제를 하나 만들어보자.

파일을 하나 열어서 모든 라인의 숫자를 더한 합을 돌려주는 코드를 만들어보겠다.

우선 숫자가 담긴 numbers.txt파일을 하나 먼저 만든다.

1
2
3
4

모든 라인의 숫자의 합은 10이다. numbers.txt파일의 경로를 주면 10을 돌려받는 메서드에 대한 테스트를 제작해보자

public class CalculatorTest {  

    @Test  
    public void sumOfNumbers() throws IOException {  
        Calculator calculator = new Calculator();  
        int sum = calculator.calcSum("C:\\tobyTest\\src\\test\\java\\springbook\\callbackSample\\numbers.txt");  
        assertThat(sum, is(10));  
    }  
}

그럼 이제 Calculator 클래스를 생성해보고 calcSum() 메서드를 구현해보자

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;
    }
}

테스트를 돌려보면 정상적으로 실행은 되나, 약간의 문제가 있다. calcSum() 메서드가 시행 중에 예외가 발생하면, 파일이 정상적으로 닫히지 않고 메서드를 빠져나가는 문제가 발생한다. 따라서 try/catch/finally 블록을 적용해서 어떤 경우라도 파일을 반드시 닫아주도록 만들어보자.

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());
                }
            }
        }
    }
}

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

자, 이제 파일에 있는 모든 숫자의 곱을 계산하는 기능을 추가해보자. 그리고 앞으로도 여러가지 방식을 처리하는 기능이 계속 추가될 예정이다.
그럼 그럴 때마다 try/catch문을 복사 붙여넣기해서 사용할 것인가? 그게 과연 정담일까?
템플릿/콜백 패턴을 적용하면 굳이 복붙을 하지 않아도 충분히 반복되는 작업을 추출할 수 있다.
적용을 하기 전에, 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇이고, 콜백이 템플릿에게 돌려줄 내용은 무엇인지 생각해보자. 템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 가장 중요하다. 그에 따라 콜백의 인터페이스를 적용해야 하기 때문이다.
일단, 템플릿이 파일을 열고 각 라인을 읽어올 수 있는 BufferdReader을 만들어서 콜백에게 전달해주고, 콜백이 각 라인을 읽어서 알아서 처리한 후에 최종 결과만 템플릿에게 돌려주게끔 제작해보자

public interface BufferedReaderCallback {
    Integer doSomethingWithReader(BufferedReader br) throws IOException;
}

이제 템플릿 부분을 메서드로 분리하고 calsSum() 메서드를 수정해보자

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);
    }

    public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            int ret = callback.doSomethingWithReader(br);

            return ret;
        }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());
                }
            }
        }
    }
}

BufferdReader을 만들어서 넘겨주는 것과 그 외의 모든 번거로운 작업에 대한 작업 흐름은 템플릿에서 진행하고, 준비된 BufferdReader를 이용해 작업을 수행하는 부분은 콜백을 호출해서 처리하도록 만들었다.
템플릿으로 분리한 부분을 제외한 나머지 코드를 BufferdReaderCallBack 인터페이스로 만든 익명 내부 클래스에 담은 후 처리할 파일의 경로와 함께 준비된 익명 내부 클래스의 오브젝트 템플릿에 전달한다.

이제 파일 있는 숫자의 곱을 구하는 메서드도 제작해보자. 똑같이 템플릿/콜백을 이용해 만들면 된다.
우선 테스트를 먼저 제작한 후에 구현을 해보자.

public class CalculatorTest {

    Calculator calculator;
    String numFilepath;

    @Before
    public void setUp() {
        this.calculator = new Calculator();
        this.numFilepath = "C:\\tobyTest\\src\\test\\java\\springbook\\callbackSample\\numbers.txt";
    }

    @Test
    public void sumOfNumbers() throws IOException {
        assertThat(calculator.calcSum(this.numFilepath), is(10));
    }

    @Test
    public void multiplyOfNumbers() throws IOException {
        assertThat(calculator.calcMultiply(this.numFilepath), is(24));
    }
}

calculator 오브젝트와 파일 경로를 @Before 메서드에서 미리 픽스처로 만들어두었다.

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);
    }

    public Integer 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);
    }

    public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            int ret = callback.doSomethingWithReader(br);

            return ret;
        }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());
                }
            }
        }
    }
}

템플릿/ 콜백 재설계

템플릿/콜백 패턴을 적용해서 파일을 읽어 처리하는 코드를 상당히 깔끔하게 정리할 수 있었다. 허나, calcSum()과 calcMultiply()에 나오는 두 개의 콜백을 비교해보자. 여기서 또 다시 어떤 공통적인 패턴이 발견되진 않는지 주의 깊게 관찰해보자.

Integer sum = 0;
String line = null;
while ((line = br.readLine())!= null) {
    sum += Integer.valueOf(line);
}
return sum;
Integer multiply = 1;
String line = null;
while((line = br.readLine()) != null) {
    multiply *= Integer.valueOf(line);
}
return multiply;           

두 개의 코드가 아주 유사함을 알 수 있다.

  1. 먼저 결과를 저장할 변수(sum, multiply)를 초기화 하고,
  2. BufferedReader를 이용해 파일의 마지막 라인까지 순차적으로 읽으면서,
  3. 각 라인에서 읽은 내용을 결과를 저장할 변수 값과 함께 계산 하다가,
  4. 파일을 다 읽었으면 결과를 저장하고,
  5. 변수의 값을 리턴한다.

변하는 코드의 경계를 찾고, 그 경계를 사이에 두고 주고받는 일정한 정보가 있는지 확인하면 쉽게 분리할 수 있다.
파일의 각 라인과 현재까지 계산한 값을 넘겨주는 부분을 인터페이스를 통해 콜백해보자

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

LineCallback을 사용하는 템플릿 메서드를 생성해보자

 public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            Integer res = initVal;
            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());
                }
            }
        }

}

중간에 반복문을 통해 콜백을 여러 번 반복적으로 호출하는 구조라는 것을 알 수 있다.
이제 calcSum()과 calcMultiply를 방금 제작한 템플릿을 사용하도록 수정해보자

public class Calculator {
    public Integer 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 calcMultiply(String filepath) throws IOException {
        LineCallback multiplyCallback =
                new LineCallback() {
                    @Override
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value * Integer.valueOf(line);
                    }
                };
        return lineReadTemplate(filepath, multiplyCallback, 1);
    }

    public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            Integer res = initVal;
            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());
                }
            }
        }

    }
}

핵심 코드 단 한 줄을 제외하고든 모두 템플릿에다가 넣었다.

제네릭스를 이용한 콜백 인터페이스

현재 LineCalback과 lineReadTemplate()는 템플릿과 콜백아 만들어내는 결과가 Integer 타입으로 고정되어있다. 만약 파일을 라인 단위로 처리해서 만드는 결과의 타입을 다양하게 가져가고 싶다면, 자바 언어에 타입 파라미터라는 개념을 도입한 제네릭스(Generics)를 이용하면 된다.
제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메서드를 정의할 수 있다.
파일의 각 라인에 있는 문자를 모두 연결해서 하나의스트링으로 돌려주는 기능을 만든다고 생각해보자. 이번에는 템플릿이 리턴하는 타입이 스트링이어야한다. 콜백의 작업 결과도 스트링이어야한다. 어떻게 인터페이스에 타입을 다양하게 처리할 수 있을까?

먼저, 콜백 인터페이스를 수정하자. 콜백 메서드의 리턴 값과 파라미터 값의 타입을 제네릭 타입 파라미터(T)로 선언한다.

public interface LineCallback<T> {
    T doSomethingWithLine(String line, T value);
}

그 다음, 템플릿 메서드도 수정해주자

    public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            T res = initVal;
            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());
                }
            }
        }

    }

수정한 템플릿을 이용해 수정한 calcSum()과 calcMultiply(), 그리고 새롭게 구현한 concatenate() 메서드까지 같이 보자

public class Calculator {
    public Integer calcSum(String filepath) throws IOException {
        LineCallback<Integer> sumCallback =
                new LineCallback<Integer>() {
                    @Override
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value + Integer.valueOf(line);
                    }
                };
        return lineReadTemplate(filepath, sumCallback, 0);
    }

    public Integer calcMultiply(String filepath) throws IOException {
        LineCallback<Integer> multiplyCallback =
                new LineCallback<Integer>() {
                    @Override
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value * Integer.valueOf(line);
                    }
                };
        return lineReadTemplate(filepath, multiplyCallback, 1);
    }

    public String concatenate(String filepath) throws IOException {
        LineCallback<String> concatenateCallback =
                new LineCallback<String>() {
                    @Override
                    public String doSomethingWithLine(String line, String value) {
                        return value + line;
                    }
                };
        return lineReadTemplate(filepath, concatenateCallback, "");
    }

    public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            int ret = callback.doSomethingWithReader(br);

            return ret;
        } 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());
                }
            }
        }
    }

    public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            T res = initVal;
            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());
                }
            }
        }

    }
}

람다식을 이용하면 더 간결하게 표현할 수 있다.

public class Calculator {
    public Integer calcSum(String filepath) throws IOException {
        return lineReadTemplate(filepath,
                (line, value) -> value + Integer.valueOf(line),
                0);
    }

    public Integer calcMultiply(String filepath) throws IOException {

        return lineReadTemplate(filepath,
                (line, value) -> value * Integer.valueOf(line),
                1);
    }

    public String concatenate(String filepath) throws IOException {
        return lineReadTemplate(filepath,
                (line, value) -> value + line,
                "");
    }

    public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(filepath));
            T res = initVal;
            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());
                }
            }
        }

    }
}
728x90