Template Method 패턴은 객체지향 설계에서 제공되는 행동형 패턴 중 하나로, 알고리즘의 구조를 정의하고, 그 세부 구현은 서브클래스에서 정의하는 방식입니다. 즉, 어떤 처리 과정의 전체 흐름을 정의해 두고, 그 세부적인 단계만을 자식 클래스에서 구체화하는 방식으로 동작합니다. 이 패턴은 주로 중복된 코드의 재사용성을 높이고, 알고리즘을 일정하게 유지하기 위해 활용됩니다.
Template Method 패턴의 주요 구성 요소
Template Method 패턴은 주로 두 가지 주요 요소로 나눌 수 있습니다:
추상 클래스(Abstract Class): 알고리즘의 기본 뼈대를 정의합니다. 알고리즘의 흐름을 설정하는 "템플릿 메소드"를 포함하며, 구체적인 단계는 서브클래스에서 구현하도록 되어 있습니다.
구체적인 클래스(Concrete Class): 추상 클래스에서 정의한 템플릿 메소드에 포함된 구체적인 알고리즘 단계를 구현합니다.
Template Method 패턴의 동작 원리
Template Method 패턴에서 가장 중요한 요소는 바로 템플릿 메소드입니다. 이 메소드는 알고리즘의 기본적인 흐름을 정의하고, 특정 단계의 실행을 서브클래스에 위임합니다. 예를 들어, 어떤 데이터를 처리하는 알고리즘에서 '데이터 읽기', '처리', '출력' 단계를 거친다고 할 때, 이 흐름을 템플릿 메소드에서 정의하고, 각 단계별 세부 사항은 서브클래스에서 구현하는 방식입니다.
예시
간단한 예시로, 커피를 만드는 알고리즘을 고려해봅시다. 커피를 만드는 과정은 비슷하지만, 커피의 종류에 따라 세부적인 과정이 달라질 수 있습니다.
abstract class CoffeeTemplate {
// 템플릿 메소드
public final void makeCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}
// 기본 알고리즘 단계
public void boilWater() {
System.out.println("Boiling water");
}
public void brewCoffeeGrinds() {
System.out.println("Brewing coffee grounds");
}
public void pourInCup() {
System.out.println("Pouring coffee into cup");
}
// 자식 클래스에서 구현해야 할 부분
public abstract void addCondiments();
}
class Tea extends CoffeeTemplate {
@Override
public void addCondiments() {
System.out.println("Adding lemon to tea");
}
}
class Coffee extends CoffeeTemplate {
@Override
public void addCondiments() {
System.out.println("Adding sugar and milk to coffee");
}
}
위 예시에서 makeCoffee() 메소드는 커피를 만드는 기본 흐름을 정의합니다. addCondiments() 메소드는 추상 메소드로, 커피와 차에 따라 다른 양념을 추가하는 세부 구현을 서브클래스에서 담당합니다.
Template Method 패턴의 장점
코드 재사용성: 알고리즘의 구조는 상위 클래스에 정의되고, 세부 구현만 자식 클래스에서 수정하므로 코드 중복을 줄일 수 있습니다.
일관성 유지: 알고리즘의 구조가 고정되어 있기 때문에, 알고리즘의 흐름이 일관되게 유지됩니다.
유연성 제공: 알고리즘의 일부 단계만 수정할 수 있어 유연하게 확장할 수 있습니다.
Template Method 패턴의 단점
상속 의존성: 추상 클래스에 의존해야 하므로, 상속 관계가 복잡해질 수 있습니다.
세부 구현에 대한 제한: 기본적인 알고리즘은 상위 클래스에 정의되지만, 세부 구현에 대한 유연성이 떨어질 수 있습니다.
사용 시기
Template Method 패턴은 다음과 같은 상황에서 유용하게 사용됩니다:
알고리즘의 구조가 거의 비슷하고, 일부 단계만 달라지는 경우.
코드의 재사용성을 높이고, 알고리즘 흐름을 일관성 있게 유지하고자 할 때.
여러 서브클래스에서 공통된 알고리즘 흐름을 따르되, 세부적인 구현만 달리 하고자 할 때.
결론
Template Method 패턴은 알고리즘의 구조를 상위 클래스에서 정의하고, 세부적인 구현을 자식 클래스에서 다르게 구현할 수 있도록 함으로써 코드의 중복을 줄이고, 알고리즘 흐름을 일관성 있게 유지할 수 있게 해줍니다. 이 패턴은 특히 알고리즘의 구조가 동일하지만, 세부 구현만 다른 경우에 매우 유용하게 사용됩니다.
public interface ValidationStrategy {
boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public class IsNumeric implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("\\d+");
}
}
public class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v) {
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s);
}
}
public class LegacyMain {
public static void main(String[] args) {
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaa");
System.out.println(b1);
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("hello");
System.out.println(b2);
}
}
Lambda로 리팩터링 예제
public class LambdaMain {
public static void main(String[] args) {
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaaa");
System.out.println(b1);
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");
System.out.println(b2);
}
}
ValidationStrategy 는 함수형 인터페이스며 Predicate과 같은 함수 디스크립터를 갖고 있음을 파악할 수 있음.
public class Kbank implements OnlineBanking{
@Override
public void makeCustomerHappy(Customer c) {
System.out.println(c.getName() + "케이뱅크 오버라이딩");
}
}
public class NH implements OnlineBanking{
@Override
public void makeCustomerHappy(Customer c) {
System.out.println(c.getName() + "농협 오버라이딩");
}
}
public class LagacyMain {
public static void main(String[] args) throws Exception {
OnlineBanking kBanking = new Kbank();
kBanking.processCustomer(1);
kBanking.processCustomer(2);
OnlineBanking nhBanking = new NH();
nhBanking.processCustomer(1);
nhBanking.processCustomer(2);
}
}
Lambda로 리팩터링 예제
public class OnlineBankingLambda {
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) throws Exception {
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
}
public class LambdaMain {
public static void main(String[] args) throws Exception {
new OnlineBankingLambda().processCustomer(1, (Customer c) -> {
System.out.println(c.getName() + "케이뱅크 오버라이딩");
});
new OnlineBankingLambda().processCustomer(2, (Customer c) -> {
System.out.println(c.getName() + "케이뱅크 오버라이딩");
});
new OnlineBankingLambda().processCustomer(1, (Customer c) -> {
System.out.println(c.getName() + "농협 오버라이딩");
});
new OnlineBankingLambda().processCustomer(2, (Customer c) -> {
System.out.println(c.getName() + "농협 오버라이딩");
});
}
}
public interface Observer {
void notify(String tweet);
}
public class Guardian implements Observer{
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("queen")) {
System.out.println("Yet more news from London..." + tweet);
}
}
}
public class LeMonde implements Observer{
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news!" + tweet);
}
}
}
public class NYTimes implements Observer {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY!" + tweet);
}
}
}
public class Feed implements Subject{
private final List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer o) {
this.observers.add(o);
}
@Override
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
public class LagacyMain {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
}
}
Lambda로 리팩터링 예제
public class LambdaMain {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver((String tweet) -> {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY!" + tweet);
}
});
f.registerObserver((String tweet) -> {
if (tweet != null && tweet.contains("queen")) {
System.out.println("Yet more news from London..." + tweet);
}
});
f.registerObserver((String tweet) -> {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news!" + tweet);
}
});
f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
}
}
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
public T handle(T input){
T r = handleWork(input);
if(successor != null){
return successor.handle(r);
}
return r;
}
abstract protected T handleWork(T input);
}
public class HeaderTextProcessing extends ProcessingObject<String> {
@Override
protected String handleWork(String text) {
return "From Raoul, Mario and Alan : "+ text;
}
}
public class SpellCheckerProcessing extends ProcessingObject<String>{
@Override
protected String handleWork(String text) {
return text.replaceAll("labda", "lambda");
}
}
public class LagacyMain {
public static void main(String[] args) {
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2); //두 작업 처리 객체를 연결
String result = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result);
}
}
Lambda로 리팩터링 예제
public class LambdaMain {
public static void main(String[] args) {
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result);
}
}
public class ProductFactory {
public static Product createProduct(String name){
return switch (name) {
case "loan" -> new Loan();
case "stock" -> new Stock();
case "bond" -> new Bond();
default -> throw new RuntimeException("No such product" + name);
};
}
}
public class LagacyMain {
public static void main(String[] args) {
Product p = ProductFactory.createProduct("loan");
System.out.println(p.getClass());
}
}
Lambda로 리팩터링 예제
public class LambdaProductFactory {
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}
public static Product createProduct(String name) {
Supplier<Product> p = map.get(name);
if (p != null) return p.get();
throw new IllegalArgumentException("No such Product " + name);
}
}
public class LambdaMain {
public static void main(String[] args) {
Product p = LambdaProductFactory.createProduct("loan");
System.out.println(p.getClass());
}
}
ConcreateStrategy1~ 클래스에 해당하는 AccountLogServiceImpl 클래스 구현체
package com.smt.service.log;
import com.smt.dao.log.LogDao;
import com.smt.dto.AccountLogDto;
import com.smt.util.DateUtils;
import com.smt.util.enums.LogType;
import com.smt.vo.common.SearchListVO;
import com.smt.vo.common.SearchResultVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AccountLogServiceImpl implements LogStrategyService {
private final LogDao logDao;
@Override
public SearchResultVO getLogList(SearchListVO searchListVO) throws Exception {
List<AccountLogDto> result = new ArrayList<AccountLogDto>();
SearchResultVO daoVO = logDao.getAccoutLogList(searchListVO);
//date 포맷 변경
List<AccountLogDto> searchedList = (List<AccountLogDto>) daoVO.getSearchedList();
for (AccountLogDto dto : searchedList) {
dto.set_logTime(DateUtils.parseDateFormatHHMMSSss(dto.getLogTime()));
result.add(dto);
}
return new SearchResultVO(daoVO.getTotal(), result);
}
@Override
public SearchResultVO getLogList(SearchListVO searchListVO, String lang) throws Exception {
return null;
}
@Override
public boolean isTarget(String logTypeUrl) {
return logTypeUrl.equals(LogType.ACCOUNT.getUrl());
}
}
Context에 해당하는 Controller 클래스
또한 다음 Controller에서 @PathVariable를 활용해서 기존에 URL에 요청하는 Controller 메서드가 동작하는 구조였지만, 하나는 Controller 메서드에서 로그 type에 해당하는 상속 클래스를 Spring에 singleton으로 주입된 해당 클래스를 찾아서 초기화 하도록 구현함에 따라서 코드를 간략하게 리팩토링 하였음.
참고로, 현재는 List를 활용해서 해당 하위 클래스를 접근하는 방식이지만, 현재는 Map과 Spring에서 제공하는 @Service("account") 속성을 활용해서 클래스를 더욱 빠른 속도로 찾을 수 있게 개선해 놓은 상태임
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/log")
@Validated
@Api(value = "LogController", description = "로그 관련(참고문서 : 구글 공유 문서 독스토리 서버 ReturnCode)")
public class LogController {
private final LogService logService;
private final List<LogStrategyService> logStrategyServices;
@ApiOperation(value = "통합 로그 조회 API-정책 설정 로그 제외")
@PostMapping("/list/{type}")
public ResultVO getLogStrategy(@PathVariable("type") String logType, @Valid @RequestBody SearchListVO searchListVO) throws Exception {
log.info("[/list/{type}] :" + searchListVO.toString() + "type : " + logType);
SearchResultVO searchResult = logStrategyServices.stream()
.filter(logService -> logService.isTarget(logType))
.findFirst()
.get()
.getLogList(searchListVO);
return APIUtil.resResult(ErrorCode.SUCCESS.getErrorCode(), "통합 로그 조회가 완료되었습니다.", searchResult);
}
결론
만약 PathVariable을 이용하고 전략 패턴을 적용하지 않았다면 path Type에 따라서 if문을 통해서 해당 메서드를 호출되었을 되었을 것임
하지만, 전략 패턴을 적용함으로써 if문이 아닌 반복문(다형성)을 이용하게됨
다른 유형의 로그 유형을 구현(추가)해야할 경우 Strategy 인터페이스를 상속 받아서 ConcreateStrategy~ 클래스를 구현하면됨
즉, 전략패턴을 적용함에 따라서 SOLID의 원칙 중 OCP(Open-Closed Principle)를 준수하게 됨