본문 바로가기

spring

[토비의 스프링 3.1] 3장 - 템플릿/콜백의 응용

[토비의 스프링 3.1] 3장에 정리된 내용. 

중복된 코드를 고민하고 이를 템플릿/ 콜백 패턴을 통해 해결하는 과정이 순차적으로 정리되어 있다. 

비슷한 코드를 실무에서 어떻게 고민하고 해결해야 할지 참고하여 적용해 볼 수 있을것 같다.

익숙해 질때까지 보고 또 보면 좋을 것 같아 정리해 둔다. 

 

스프링을 사용하는 개발자라면 당연히 스프링이 제공하는 템플릿/콜백기능을 잘 사용할 수 있어야 한다. 

동시에 팀플릿/콜백이 내장된 것을 원리도 알지 못한채로 기계적으로 사용하는 경우와 적용된 패턴을 이해하고 사용하는 경우는 

큰 차이가 있다.

 

 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자.
중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다. 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 
사이에 두고 분리해서 전략패턴을 적용하고 DI로 의존관계를 관리하도록 만든다. 그런데 바뀌는 부분이 한 애플리케이션 안에서 
동시에 여러 종류가 만들어 질 수 있다면 이번엔 템플릿/콜백 패턴을 적용 하는것을 고려해볼 수 있다.

 

- 파일의 숫자 합을 계산하는 테스트  코드

public class CalcSumTest{
	@Test
    public void sumOfNumbers() 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 = null;
        try{
        	// 한 줄씩 읽기 편하게 BufferedReader 로 파일을 가져온다.
        	br = new BufferedReader(new FileReader(filepath)); 
        	Integer sum = 0; 
        	String line = null;
        	while((line = br.readLine()) != null){ // 마지막 라인까지 한줄씩 읽으며 숫자를 더한다. 
        		sum += Integer.value.of(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()); 
            }
        }
       
    }
}

 

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

 

 그런데 이번엔 파일에 있는 모든 숫자의 곱을 계산하는 기능을 추가해야 한다는 요구가 발생 했다. Calculator라는 클래스의 이름에 걸맞게 앞으로도 많은 파일에 담긴 숫자 데이터를 여러가지 방식으로 처리하는 기능이 계속 추가될 것이라는 소식도 들려왔다고 생각해보자. 

 

파일을 읽어서 처리하는 비슷한 기능이 새로 필요할 때마다 앞에서 만든 코드를 복사해서 사용할 것인가? 물론 아니어야 한다.

한두번 까지는 어떻게 넘어간다고 해도, 세번이상 반복된다면 본격적으로 코드를 개선할 시점이라고 생각해야 한다. 

객체지향 언어를 사용하고 객체지향 설계를 통해 코드를 작성하는 개발자의 기본적인 자세다. 

 

템플릿/콜백 패턴을 적용해 보자. 먼저 템플릿에 담을 반복되는 작업 흐름은 어떤것인지 살펴보자. 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇이고, 콜백이 템플릿에게 돌려줄 내용은 무엇인지도 생각해보자. 템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 가장 중요하다. 그에 따라 콜백의 인터페이스를 정의해야 하기 때문이다. 

 

- BufferedReader 를 전달 받는 콜백 인터페이스 

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

 가장 쉽게 생각해볼 수 있는 구조는 템플릿이 파일을 열고 각 라인을 읽어올 수 있는 BufferedReader 를 만들어서 콜백에게 전달해주고, 

콜백이 각 라인을 읽어서 알아서 처리한 후에 최종 결과만 템플릿에게 돌려주는 것이다. 

 

- BufferedReaderCallback을 사용하는 템플릿 메서드

public class Calculator {
    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);
            throw e;
        }finally {
            if(br != null) {
                try {
                    br.close();

                }catch (IOException e){
                    System.out.println(e.getMessage());
                }
            }
        }
    }
}

 

- 템플릿/콜백을 적용한 calcSum(), calcMultiply() 메소드

Calculator calculator = new Calculator();
int sum = calculator.fileReadTemplate(getClass().getResource("numbers.txt").getPath(), 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;
        }
  });
Calculator calculator = new Calculator();
int sum = calculator.fileReadTemplate(getClass().getResource("numbers.txt").getPath(), new BufferedReaderCallback() {
        @Override
        public Integer doSomethingWithReader(BufferedReader br) throws IOException {
            Integer multiply = 0;
            String line = null;
            while((line = br.readLine()) != null){
                multiply *= Integer.valueOf(line);
            }
            return multiply;
        }
  });

 

템플릿/콜백의 재설계

탬플릿/콜백 패턴을 적용해서 파일을 읽어 처리하는 코드를 상당히 깔끔하게 정리 할 수 있었다. 

이제 try/catch/finally 블록 없이도 파일을 안전하게 처리하는 코드를 사용할 수 있게 됐다. 여기서 또 다시 어떤 공통적인 패턴이 발견 되진 않는지 주의 깊게 관찰해 보자. 아래는 유일하게 변하는 라인이고 나머지는 동일하다. 

 

multiply *= Integer.valueOf(line);

 

sum += Integer.valueOf(line); 

 

조금만 살펴보아도 두개의 코드가 아주 유사함을 알 수 있다. 번저 결과를 저장할 변수(multiply, sum) 를 초기화 하고, BufferedReader를 이용해서 파일의 마지막 라인까지 순차적으로 읽으면서 각 라인에서 읽은 내용을 결과를 저장할 변수의 값과 함께 계산하다가,

파일을 다 읽었으면 결과를 저장하고 있는 변수의 값을 리턴한다. 

유일 하게 하는 라인을 처리하고 다시 외부로 전달 되는 것은 multiply 또는 sum과 각 라인의 숫자 값을가지고 계산한 결과이다. 

 

- 라인별 작업을 정의한 콜백 인터페이스

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) {...}
        finally {...}
 }

 

- lineReadTemplate() 을 사용하도록 수정한 calSum(), calcMutiply() 메소드

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 sumCallback = new LineCallback() {
        @Override
        public Integer doSomethingWithLine(String line, Integer value) {
            return value * Integer.valueOf(line);
        }
    };
    return lineReadTemplate(filepath, sumCallback, 1);
}
반응형