템플릿 메서드 패턴
알고리즘 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법
- 추상 클래스는 템플릿을 제공하고 하위 클래스는 구체적인 알고리즘을 사용한다.
예로, 아래와 같은 구조의 알고리즘이 있다 가정하자.
- 파일을 읽은 뒤
- 적절히 파싱한다.
- 그리고 출력한다.
위 알고리즘을 템플릿으로 제공하고 각각의 기능 (1, 2, 3) 들 중 달라질 수 있는 부분이 있다면 서브 클래스는 달라지는 부분만 직접 구현한다.
AbstractClass 의 templateMethod 메소드는 알고리즘의 구조를 표현하는 메소드이다.
템플릿 메서드 패턴 적용 전
간단하게 숫자로만 이루어진 .txt 파일을 읽어서 해당 파일의 숫자들을 모두 덧셈한 결과를 알려주는 기능을 구현해보자.
numbers.txt 파일은 아래와 같다.
1
2
3
4
5
Client
public class Client {
public static void main(String[] args) {
FileProcessor fileProcessor = new FileProcessor("number.txt");
int result = fileProcessor.process();
System.out.println(result);
}
}
파일의 Path 를 FileProcessor 에게 주고, process 메서드를 호출하면 연산 결과를 받을 수 있다.
FileProcessor
public class FileProcessor {
private String path;
public FileProcessor(String path) {
this.path = path;
}
public int process() {
try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 0;
String line = null;
while((line = reader.readLine()) != null) {
result += Integer.parseInt(line);
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
}
Client 코드를 실행하면 15 가 출력된다.
이 때 덧셈이 아닌 모든 숫자들을 곱셈해주는 기능이 필요하면 어떻게 구현할 수 있을까?
아래와 같이 새로운 클래스를 정의 한 후 기존의 FileProcessor 에서 기능을 가져와 새로 곱셈으로 바꿔줄 수 있다.
MultiplyFileProcessor
public class MultiplyFileProcessor {
private String path;
public MultiplyFileProcessor(String path) {
this.path = path;
}
public int process() {
try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 0;
String line = null;
while((line = reader.readLine()) != null) {
result *= Integer.parseInt(line); // 이 부분만 곱셈으로 바꿔준다.
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
}
문제점
FileProcessor 과 MultiplyFileProcessor 의 코드를 보면 연산자 부분만 다르고 나머진 모두 똑같다.
이러한 중복되는 코드를 템플릿 메서드 패턴을 적용하여 없애보자.
템플릿 메서드 패턴 적용 후
먼저 FileProcessor 추상 클래스를 만든다.
해당 AbstractClass 는 공통되는 로직을 그대로 가지고, 달라지는 부분만 추상 메서드로 분리한다.
public abstract class FileProcessor {
private String path;
public FileProcessor(String path) {
this.path = path;
}
public int process() {
try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 0;
String line = null;
while((line = reader.readLine()) != null) {
result = getResult(result, Integer.parseInt(line)); // 아래 추상 메서드 호출
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
protected abstract int getResult(int result, int number);
}
이제 각 ConcreteClass 들은 이를 상속하여 getResult 메서드만 구현하면 된다.
먼저 뎃셈을 하는 PlusFileProcessor 를 보자.
public class PlusFileProcessor extends FileProcessor {
public PlusFileProcessor(String path) {
super(path);
}
@Override
protected int getResult(int result, int number) {
return result += number;
}
}
곱셈을 하는 MultiplyFileProcessor 는 아래와 같다.
public class MultiplyFileProcessor extends FileProcessor {
public MultiplyFileProcessor(String path) {
super(path);
}
@Override
protected int getResult(int result, int number) {
return result *= number;
}
}
중복되는 코드들은 AbstractClass 에 모아두고 다른 부분만 각 클래스들이 구현하도록 하여 가독성을 높이고 중복은 제거시켰다.
Client 를 보면 아래와 같다.
public class Client {
public static void main(String[] args) {
PlusFileProcessor plusFileProcessor = new PlusFileProcessor("numbers.txt");
int result1 = plusFileProcessor.process();
System.out.println(result1);
MultiplyFileProcessor multiplyFileProcessor = new MultiplyFileProcessor("numbers.txt");
int result2 = multiplyFileProcessor.process();
System.out.println(result2);
}
}
템플릿 콜백 패턴
템플릿 메소드 패턴 방식을 이용하여 기존의 문제점을 해결하였지만 이 패턴과 유사한 템플릿 콜백 패턴도 존재한다.
콜백으로 상속대신 위임을 사용하는 템플릿 패턴
- 상속 대신 익명 내부 클래스 또는 람다식을 사용할 수 있다.
아래와 같이 Callback 인터페이스인 Operator 를 만든다.
@FunctionalInterface
public interface Operator {
int getResult(int result, int number);
}
FileProcessor 는 추상 클래스가 아닌 일반 클래스가 되고, process 는 Operator 타입을 인자로 받아 Operator 의 getResult 메서드를 호출하도록 구현한다.
public class FileProcessor {
private String path;
public FileProcessor(String path) {
this.path = path;
}
public final int process(Operator operator) {
try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
int result = 0;
String line = null;
while((line = reader.readLine()) != null) {
result = operator.getResult(result, Integer.parseInt(line)); // operator 의 메서드를 호출
}
return result;
} catch (IOException e) {
throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
}
}
}
Client 는 아래와 같이 구현 할 수 있다.
public class Client {
public static void main(String[] args) {
FileProcessor plus = new FileProcessor("numbers.txt");
int result1 = plus.process(((result, number) -> result += number));
System.out.println(result1);
FileProcessor multiply = new FileProcessor("numbers.txt");
int result2 = plus.process(((result, number) -> result *= number));
System.out.println(result2);
}
}
이는 클래스를 만들지 않고 위임을 사용할 수 있는 좋은 방법이다.
물론, 클래스로 Operator 를 Implements 한 클래스를 사용한 구현도 가능하다.
장점과 단점
장점
- 템플릿 코드를 재사용하고 중복 코드를 줄일 수 있다.
- 템플릿 코드를 변경하지 않고 상속을 받아서 구체적인 알고리즘을 변경할 수 있다.
단점
- 리스코프 치환 법칙을 위반할 여지가 있다.
- 하위 클래스에서 process 를 오버라이드하여 재정의 할 수 있기 때문이다.
- 알고리즘 구조가 복잡할수록 템프릿을 유지하기 어려워진다.
실무에선 어떻게 쓰이나?
자바의 서블릿
public class MyHello extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
우리가 HttpServlet 을 extends 하고 doGet 혹은 doPost 메서드를 오버라이드 하면 서블릿이 자신의 로직을 수행하다가 doGet, doPost 를 호출해야 할 때 위 클래스를 참조하여 doGet, doPost 메서드를 실행한다.
이때 해당 코드에 대한 제어건은 우리에게 없다. DI
스프링의 Configuration
public class TemplateInSpring {
public static void main(String[] args) {
// TODO 템플릿-콜백 패턴
// JdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.execute("insert");
// RestTemplate
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
headers.set("X-COM-PERSIST", "NO");
headers.set("X-COM-LOCATION", "USA");
HttpEntity<String> entity = new HttpEntity<String>(headers);
ResponseEntity<String> responseEntity = restTemplate
.exchange("http://localhost:8080/users", HttpMethod.GET, entity, String.class);
}
@Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
}
}
WebSecurityConfigureAdapter 를 extends 하고 configure 메서드를 오버라이드하면 우리가 스프링 Config 의 거대한 알고리즘 중 일부를 우리가 구현하게 되는 것이다.
참고
'디자인 패턴' 카테고리의 다른 글
비지터 패턴 (Visitor Pattern) (0) | 2022.08.13 |
---|---|
전략 패턴 (Strategy Pattern) (1) | 2022.08.13 |
상태 패턴 (State Pattern) (0) | 2022.08.13 |
메멘토 패턴 (Memento Pattern) (0) | 2022.08.12 |
중재자 패턴 (Mediator pattern) (0) | 2022.08.11 |