본문 바로가기

Spring

Toby's Spring - Chap 3 - Template/Callback practice

템플릿/콜백으로 변환하는 사고 흐름

1. 고정 된 작업 흐름을 갖고 여기저기서 자주 반복되는 코드 발견!

2. 중복되는 코드를 분리할 방법 생각해본다.

3. 그 중 일부 작업을 필요에 따라 바꿔야 한다면 인터페이스를 두고 전략 패턴을 적용한다.

4. 만약 바뀌는 부분이 한 어플리케이션 안에서 동시에 여러 종류가 만들어질 수 있으면 템플릿/콜백 패턴을 적용해본다.

 

예제

파일의 숫자 합을 계산하는 코드의 테스트가 있습니다.

@SpringBootTest
public class CalcSumTest {
	@Test
	public void sumOfNumbers() throws IOException {
		Calculator calculator = new Calculator();
		int sum = calculator.calcSum(getClass()
			.getResource("number.txt").getPath());
		assertEquals(sum, 10);
	}
}

 

이제 같이 수정해 볼 Calculator 코드입니다.

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

 

위의 코드는 에러시에 BufferedReader 리소스를 반환하지 못합니다.

try with resource 문을 써서 예외 상황을 대비하겠습니다.

public Integer calcSum(String filepath) throws IOException {
	try(BufferedReader 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;
	}
}

 

이제 모든 라인의 숫자를 곱하는 기능을 추가합니다.

위의 메소드에 템플릿/콜백을 적용하려면 어떤 부분이 변하고 변하지 않느지 확인해야 합니다.

계산을 하는 부분만 변하는군요!

해당 부분을 뽑아내서 인터페이스를 만들어 봅시다.

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

 

이제 템플릿에서는 해당 인터페이스를 파라미터로 받고 코드에 적용시켜야 합니다.

public Integer fileReadTemplate(String filepath, BufferedReaderCallback callback) throws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader(filepath))){
		int ret = callback.doSomethingWithReader(br);
		return ret;
	} catch (IOException e){
		System.out.println(e.getMessage());
		throw e;
	}
}

 

이제 템플릿/콜백 패턴을 사용할 calcSum 코드를 변경해 봅시다.

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 interface LineCallback {
	Integer doSomethingWithLine(String line, Integer value);
}

파일의 한 라인과 현재까지 계산한 값을 받고 새로운 계산결과를 돌려주는 역할을 하는 메소드입니다.

 

해당 콜백을 쓰는 템플릿을 다시 정의 해보겠습니다.

 

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

 

이제 파일이름, 콜백, 초기화 값을 받는 템플릿이 되었습니다.

while 루프를 돌면서 반복적으로 콜백을 호출하는 구조입니다.

 

해당 템플릿을 사용하는 calcSum(), calcMulyiply()를 구현해보겠습니다.

이번엔 Java8 버전에 추가된 람다를 사용해 보겠습니다.

람다를 사용하려면 인터페이스는 함수형 인터페이스로 선언되어야합니다. 함수형 인터페이스에는 @FunctionInterface 언노테이션이 붙어야하는데 이 어노테이션은 함수형 인터페이스는 단 하나의 추상 메소드를 가진다 (Single Abstract Method, SAM) 를 지켜줍니다. 규칙 위반 시 컴파일러가 에러를 뱉습니다.

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

 

해당 인터페이스를 사용한 클라이언트 입니다.

public Integer calcSum(String filepath) throws IOException {
	LineCallback sumCallback = (line, value) -> value + Integer.parseInt(line);
	return lineReadTemplate(filepath, sumCallback, 0);
}

public Integer calcMultiply(String filepath) throws IOException {
	LineCallback multiplyCallback = (line, value) -> value * Integer.parseInt(line);
	return lineReadTemplate(filepath, multiplyCallback, 0);
}

 

여기서 만약 Integer가 아닌 다른 형식의 값을 사용하고 싶으면 어떻게 해야 할까요 ?

Generics를 이용해 봅시다.

 

Generics를 이용한 콜백 인터페이스

제네릭은 자바 5버전에서 추가되었는데 고정된 형식이 아닌 다양한 형식의 데이터를 파라미터로 받고

반환값으로 돌려줄 수 있습니다.

 

타입 파라미터를 적용해서 인터페이스를 수정해 보겠습니다.

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

 

이제 여러가지 타입을 활용할 준비가 되었습니다.

템플릿에도 타입 파라미터를 적용시켜 보겠습니다.

public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader(filepath))){
		T res = initVal;
		String line;
		while((line = br.readLine()) != null){
			res = callback.doSomethingWithLine(line, res);
		}
		return res;
	} catch (IOException e){
		System.out.println(e.getMessage());
		throw e;
	}
}

 

다음은 제네릭을 적용한 클라이언트 코드입니다.

 

public Integer calcSum(String filepath) throws IOException {
	LineCallback<Integer> sumCallback = (line, value) -> value + Integer.parseInt(line);
	return lineReadTemplate(filepath, sumCallback, 0);
}

public Integer calcMultiply(String filepath) throws IOException {
	LineCallback<Integer> multiplyCallback = (line, value) -> value * Integer.parseInt(line);
	return lineReadTemplate(filepath, multiplyCallback, 0);
}

public String concatenate(String filepath) throws IOException {
	LineCallback<String> concatenateCallback = (line, value) -> value + Integer.parseInt(line);
	return lineReadTemplate(filepath, concatenateCallback, "");
}

 

이번 포스트에서는 리소스를 반환해야하는 코드에 템플릿/콜백 패턴을 적용시켜 보았습니다.

감사합니다.

'Spring' 카테고리의 다른 글

Toby's Spring - Chap 6 - AOP  (0) 2020.07.19
Toby's Spring - Chap 4 - Exception  (0) 2020.06.29
Spring - Assert  (0) 2020.06.28
Toby's Spring - Chap 3 - Template  (0) 2020.06.23
Toby's Spring - Chap 2 - Test  (0) 2020.06.23