✅ 자주 반복되는 코드가 있다면?
자주 반복되는 코드, 즉 중복된 코드가 있다면 앞에서 배운 것들을 통해 이를 분리하는 방법을 생각해낼 수 있다.
- 먼저 메소드로 분리하는 간단한 방법을 시도해본다.
- 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 사이에 두고 전략패턴과 DI를 사용한다.
- 바뀌는 부분이 한 어플리케이션 안에서 동시에 여러 종류가 만들어진다면 템플릿/콜백 패턴을 적용한다.
✅ try-catch-finally 응용하기
간단한 템플릿/콜백 예제를 만들어서 응용해보자.
파일을 하나 열어서 모든 라인의 숫자를 더한 합을 돌려주는 계산기 코드를 만들어 보자.
👉 갑자기 파일 입출력과 관련된 클래스가 나와서 당황할 수도 있지만, 코드를 자세히 살펴보면 전혀 어려운 코드가 아니다.
- numbers.txt
- Calculator
package com.jhcode.spring.ch3.learningtest.template; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; 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; } }
- Integer calcSum(String filepath)
calcSum()
메서드는filepath
라는 파일 경로를 매개변수로 받는다. 파일을 읽어와서 숫자들의 합을 계산하고, 정수 값을 반환한다.IOException
은 파일 입출력 시 발생할 수 있는 예외를 처리하기 위한 선언이다.
- BufferedReader br = new BufferedReader(new FileReader(filepath));
BufferedReader
객체를 생성하여 파일을 읽기 위한 준비를 한다.FileReader
는 주어진 파일 경로로부터 문자를 읽는 역할을 한다.
- while((line = br.readLine()) != null)
파일에서 한 줄씩 읽어오는 반복문을 시작한다.
readLine()
메서드를 사용하여 파일의 한 줄을 읽어 온다. 한 줄씩 읽어온 문자의 값을 sum에 저장한다.
전부 더해진 값인 sum return하여 다시 반환한다.
- Integer calcSum(String filepath)
- CalcSumTest
package com.jhcode.spring.ch3.learningtest.template; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import org.junit.jupiter.api.Test; public class CalcSumTest { @Test public void sumOfNumbers() throws IOException { Calculator calculator = new Calculator(); int sum = calculator.calcSum(getClass().getResource("numbers.txt").getPath()); assertEquals(10, sum); } }
- getClass().getResource("numbers.txt")
getClass()
메서드는 현재 코드가 속한 클래스(CalcSumTest
)의Class
객체를 반환한다. 이는Class<CalcSumTest>
와 같은 형태가 된다.Class
객체는 제네릭을 사용하여 특정 클래스에 대한 타입 정보를 담을 수 있다.getClass()
를 통해서Class
객체를 반환하는 이유는Class
객체로부터 “numbers.txt” 파일을 찾기 위함이다.getResource("numbers.txt")
는 현재 클래스의 클래스 경로에서 "numbers.txt" 파일을 찾는 것을 의미한다. 이렇게 찾은 파일에 대한 URL을 반환하고 있다.
- .getPath()
getPath()
메서드는 파일의 경로(URL)를 문자열로 반환한다.
- calculator.calcSum(...)
Calculator
객체의calcSum()
메서드를 호출한다. 이 때, 앞서 얻은 "numbers.txt" 파일의 경로를 전달한다.
- assertEquals(10, sum);
txt 파일의 한 줄씩 값을 읽어온 것과 10의 값이 같은지 비교해본다.
👉 test를 해보면 잘 통과하는 것을 확인해 볼 수 있다.
- getClass().getResource("numbers.txt")
🔹try-catch-finally 적용
calcSum() 메소드도 파일을 읽거나 처리하다가 예외가 발생할 수 있다. 이때 예외가 발생하면 파일이 정상적으로 닫히지 않고, 메소드를 빠져나가는 문제가 발생한다. 따라서 try-catch-finally를 적용해서 예외가 발생하더라도 반드시 파일이 닫히도록 해보자.
- Calculator
package com.jhcode.spring.ch3.learningtest.template; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class Calculator { public Integer calcSum(String filepath) throws IOException { //finally 구문에서 사용해줘야하기 때문에 상단에 선언한다. 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 (Exception e) { System.out.println(e.getMessage()); throw e; } finally { if(br != null) { //BufferedReader 오브젝트가 생성되기 전에 예외가 발생될 수 있어, null 체크를 먼저해야 한다. try {br.close();} catch (IOException e) {System.out.println(e.getMessage());} } } } }
✅ 템플릿/콜백 설계, 중복 제거
모든 숫자의 곱을 계산하는 기능과 더불어 많은 파일에 담긴 숫자 데이터를 여러 가지 방식으로 처리하는 기능이 계속 추가될 때 어떻게 템플릿/콜백 메소드를 설계해야 할까?
먼저 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇이고, 콜백이 템플릿에게 돌려줄 내용은 무엇인지 생각해보자.
- 템플릿이 콜백에게 전달해줄 내부의 정보는, filePath를 통해 생성한
BufferedReader
객체이다.
- 콜백이 템플릿에게 돌려줄 내용은
BufferedReader
객체를 통해서 파일의 정보를 읽고, 읽은 데이터로 로직을 수행하여 반환된 정수 값이다.
- BufferedReaderCallback
package com.jhcode.spring.ch3.learningtest.template; import java.io.BufferedReader; import java.io.IOException; public interface BufferedReaderCallback { Integer doSomethingWithReader(BufferedReader br) throws IOException; }
위의 인터페이스 타입의
BufferedReaderCallback
은 Callback 오브젝트로써 익명 내부 클래스가 구현할 각각의 기능들을 모아둔 것이다. 이를 통해 Callback 오브젝트를 사용할 때, 익명 내부 클래스는 각 기능에 맞게 오버라이드하여 해당 내용을 구현할 수 있다.
- Calculator
- fileReadTemplate() ⇒ 템플릿 메소드
//템플릿, -> 변경되지 않는 부분 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()
메소드 안에 있던 변경되지 않는 부분을 따로 메소드로 분리하였다. 템플릿 메소드 패턴을 적용한 것이다. 이를 통해 변경되지 않는 부분은 템플릿 메소드를 호출하여 사용하고, 변경되는 부분은 콜백 패턴으로 구현함으로써 재사용성을 높일 수 있다.
- calcSum() ⇒ 콜백 적용
public Integer calcSum(String filepath) throws IOException { //콜백 오브젝트, 익명 내부 클래스이다. BufferedReaderCallback sumCallback = new BufferedReaderCallback() { //변경되는 부분, doSomethingWithReader을 오버라이드하여 재정의하여 콜백 동작을 수행한다. 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); }
calcSum()
메소드는 클라이언트이다. 클라이언트의 역할은 콜백 오브젝트를 생성하고, 템플릿 메소드를 호출하는 것이다. 위의 코드에서는 그 결과를 클라이언트가 받는 것이 아니라calcSum()
메소드를 호출한 다른 클래스에서 결과를 받아야 하기 때문에 return을 통해서 템플릿 메소드를 호출시킨다.인터페이스인
BufferedReaderCallback
에 정의된doSomthingWithReader()
메소드를 오버라이드하여 익명 내부 클래스를 생성할 때, 기능에 맞는 로직을 구현할 수 있다.
- calcMultiply() ⇒ 곱셉 연산 기능 추가
public Integer calcMultiply(String filePath) throws IOException { //콜백 오브젝트 BufferedReaderCallback multiplyCallback = new BufferedReaderCallback() { 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); }
오직 익명 내부 클래스, 콜백 오브젝트의 코드만 변경함으로써 중복되는 모든 코드들을 작성하지 않고도 안전한 코드를 작성할 수 있다.
- fileReadTemplate() ⇒ 템플릿 메소드
- CalcSumTest
package com.jhcode.spring.ch3.learningtest.template; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class CalcSumTest { Calculator calculator; String numFilepath; //파일 이름과 사용되는 객체가 테스트마다 중복되기 때문에 테스트 전에 실행하는 @BeforeEach 어노테이션으로 따로 뺴두었다. @BeforeEach public void setUp() { this.calculator = new Calculator(); this.numFilepath = getClass().getResource("numbers.txt").getPath(); } @Test public void sumOfNumbers() throws IOException { assertEquals(10, calculator.calcSum(numFilepath)); } @Test public void multiplyOfNumbers() throws IOException { assertEquals(24, calculator.calcMultiply(numFilepath)); } }
파일 이름과 사용되는 객체(
Calculator
)가 테스트마다 중복되기 때문에 테스트 전에 실행하는 @BeforeEach 어노테이션으로 따로 중복을 방지했다. 지금은 두 개 뿐이지만 앞으로 기능을 계속 추가해 나가면 더 많아질 코드를 사전에 감소시킬 수 있다.
✅ 템플릿/콜백의 재설계
calcSum()
과 calcMultiply()
에 나오는 두 개의 콜백을 비교해 보자.
public Integer doSomethingWithReader(BufferedReader br) throws IOException{
Integer sum = 0;
String line = null;
while((line = br.readLine()) != null) {
sum += Integer.valueOf(line);
}
return sum;
}
public Integer doSomethingWithReader(BufferedReader br) throws IOException {
Integer multiply = 1;
String line = null;
while((line = br.readLine()) != null) {
multiply *= Integer.valueOf(line);
}
return multiply;
}
여기서 바뀌는 코드는 4번째 줄인 multiply *= Integer.valueOf(line); 와 sum += Integer.valueOf(line); 뿐이다. 이를 다시 템플릿/콜백 패턴에 적용할 수 있지 않을까?
- LineCallback
package com.jhcode.spring.ch3.user.dao; public interface LineCallback { Integer doSomethingWithLine(String line, Integer value); }
익명 내부 클래스가 구현할 인터페이스를 선언한다. 인터페이스 선언과 동시에 익명 내부 클래스를 생성함으로써 인터페이스 타입의 변수가 참조하게 된다. 실제로 참조하는 것은 익명 내부 클래스 객체이다.
- lineReadTemplate(), 템플릿 메소드 변경
//템플릿, -> 변경되지 않는 부분 public Integer lineReadTemplate(String filepath, LineCallback callback, Integer 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());} } } }
파일의 한 줄을 읽는 작업까지 템플릿에서 할 수 있도록 변경하였다. 계산 결과를 담을 변수와 초기화할 값도 파라미터로 통해 전달 받게 만들었다. 특이한 점은 while 루프 안에서 콜백 메소드를 호출한다는 점이다. 반복적으로 콜백 메소드를 호출하는 구조가 된 것이다.
- sumCallback, multiplyCallback 익명 내부 클래스 변경
public Integer calcSum(String filepath) throws IOException { //콜백 오브젝트, 익명 내부 클래스이다. LineCallback sumCallback = new LineCallback() { //콜백 메소드는 오직 파일의 읽은 한 줄의 값을 가져와서 더하는 작업만 실시한다. 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() { public Integer doSomethingWithLine(String line, Integer value) { return value + Integer.valueOf(line); } }; return lineReadTemplate(filePath, multiplyCallback, 0); }
이제 콜백 오브젝트는 템플릿에서 파일의 한 줄의 읽은 값을 가져와서 오로지 연산하는 기능만 수행한다.
✅ 제네릭스를 이용한 콜백 인터페이스
지금까지 적용한 템플릿/콜백 패턴으로 만들어내는 결과는 Integer 타입ㅇ로 고정되어 있다. 만약 만들어내는 결과의 타입을 다양하게 가져가고 싶다면, 제네릭스를 이용하면 된다.
- LineCallback<T>
package com.jhcode.spring.ch3.user.dao; public interface LineCallback<T> { T doSomethingWithLine(String line, T value); }
T는 일종의 타입 변수로, 실제 사용될 타입은
LineCallback<T>
를 구현하는 클래스에서 결정됩니다. T는 어떤 타입이든 될 수 있으며,doSomethingWithLine()
메서드에서 해당 타입을 사용할 수 있습니다. 익명 내부 클래스에서 결정하게 됩니다.
- lineReadTemplate()
//템플릿, -> 변경되지 않는 부분 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());} } } }
lineReadTemplate() 메소드는 타입 파라미터 T를 갖는 인터페이스 LineCallback 타입의 오브젝트와, T 타입의 초기값 initVal을 받고 T 타입의 변수 res를 정의한다.
T 타입 파라미터로 선언된 LineCallback의 메소드를 호출해서 처리한 후 그 결과 값으로 T 타입의 결과를 리턴 받는 콜백 메소드가 되는 것이다.
여기서 T는 이 템플릿 메소드를 호출한 곳에서 결정된다. 어떤 타입으로도 가능하다. 제네릭스를 적용한 템플릿/콜백을 호출해보자.
- Calculator
package com.jhcode.spring.ch3.learningtest.template; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import com.jhcode.spring.ch3.user.dao.LineCallback; 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); } //== 제네릭 Integer ==// public Integer calcMultiply(String filePath) throws IOException { //콜백 오브젝트 LineCallback<Integer> multiplyCallback = new LineCallback<Integer>() { public Integer doSomethingWithLine(String line, Integer value) { return value * Integer.valueOf(line); } }; return lineReadTemplate(filePath, multiplyCallback, 1); } //== 제네릭을 사용하여 String으로 처리 ==// public String concatenate(String filepath) throws IOException { LineCallback<String> concatenateCallback = new LineCallback<String>() { public String doSomethingWithLine(String line, String value) { return value + line; } }; return lineReadTemplate(filepath, concatenateCallback, ""); } //템플릿, -> 변경되지 않는 부분 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());} } } } }
다른 메소드들도 제네릭을 선언했다. String 클래스의 + 연사자 오버라이딩을 통해서 두 문자열은 합쳐지게 된다. 따라서 결과는 “1234” 문자열이 된다.
- CalSumTest()
@Test public void concatenate() throws IOException { assertEquals("1234", calculator.concatenate(numFilepath)); }
📖토비 스프링 3.1 -p247~259
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T