스프링과 디자인 패턴 - 템플릿 메서드 패턴

이원석·2024년 2월 19일

Spring

목록 보기
13/20
post-thumbnail
*인프런 김영한 강사님의 강좌를 참고하여 정리한 내용입니다.*

로그 추적기 같이 모든 요청단에서 필요한 로직은 공통 관심사항(AOP)이라고 볼 수 있다. 이렇게 공통 관심사항이 각 레이어 마다 코드로 작성되어 있다면 유지보수가 굉장히 힘들고, 또한 핵심 기능(비즈니스 로직) 보다 부가 기능(로그 추적 로직, 트랜잭션 기능 등..)의 코드가 훨씬 더 많고 복잡해 지는 문제가 발생할 수 있다.

따라서, 이러한 핵심 기능과 부가 기능의 분리가 중요한데!! 부가 기능의 경우 동일한 패턴이 존재하는데 다양한 디자인 패턴을 통해 해결해보자~



핵심 기능과 부가 기능

예제를 통해 핵심 기능과 부가 기능에 대해 이해해보자.

[핵심 기능만 있는 경우..]
// OrderController
@GetMapping("/request")
public String request(String itemId) {
	orderService.orderItem(itemId);
    return "ok":
}

// OrderService
public void orderItem(String itemId) {
	orderRepository.save(itemId);
}


[부가 기능이 포함된 경우..]
// OrderController
@GetMapping("/request")
public String request(String itemId) {
	TraceStatus status = null;
    try {
    	status = trace.begin("OrderController.request()");
    	orderService.orderItem(itemId); // 핵심 기능
        trace.end(status);
        
    } catch (Exception e) {
    	trace.exception(status, e);
        throw e;
    }
    
    return "ok":
}

// OrderService
public void orderItem(String itemId) {
	TraceStatus status = null;
	try {
    	...
        orerRepository.save(itemId);
        ...
	} catch (Exception e) {
    	...
    }
}

해당 예제를 보면 핵심 기능 코드는 언제든지 변경될 수 있지만, 로그를 추적하는 부가 기능 코드 부분은 변하지 않음을(완전히는 아니어도 거의!) 알 수 있다.



템플릿 메서드 패턴

1. 구조

템플릿 메서드 패턴은 추상 클래스(Abstract Class)를 활용한다.

1-1. excute()

변하지 않는 부분(로그 추적, 트랜잭션 기능 등..)의 역활을 하는 메서드이다.

1-2. call()

변하는 부분(비즈니스 로직)의 역활을 하는 추상 메서드이다.

public abstract class AbstractTemplate {
	
    public void execute() {
    	// 공통 로직 살행
        ...
        
        // 비즈니스 로직 실행
        call();
        
        ...
	}
    
    protected abstract void call();
}

템플릿 메서드 패턴은 이름 그대로 템플릿을 사용하는 방식이다. 템플릿은 기준이 되는 거대한 틀, 이 곳에 불변하는 부분을 몰아둔다. 그리고 템플릿 안에서 변하는 부분인 call() 메서드를 호출해서 처리한다.



2. 방법

2-1. 추상 클래스 상속 및 오버라이딩

public class SubClassLogic1 extends AbstractTemplate {
	@Override
    protected void call() {
    	log.info("비즈니스 로직1 실행!");
    }
}

public class SubClassLogic2 extends AbstractTemplate {
	@Override
    protected void call() {
    	log.info("비즈니스 로직2 실행!");
    }
}


@Test
void templateMethod() {
	AbstractTemplate template1 = new SubClassLogic1();
    AbstractTemplate template2 = new SubClassLogic2();
    template1.execute();
    tempalte2.execute();
}

[실행결과]

공통로직 수행..
비즈니스 로직1 실행!
공통로직 수행..
비즈니스 로직2 실행!

객체지향의 특징인 다형성을 활용한 모습이다. 추상 클래스를 상속한 자식 클래스의 인스턴스를 할당했다.

template1.execute()를 호출하면 AbstractTemplate.execute()를 실행하고, 중간의 call() 메서드를 호출하는데 오버라이딩 된 메서드를 우선으로 호출한다.

따라서 각각의 call() 메서드의 구현체들이 호출된다.


2-2. 익명 클래스 or 람다

@Test
void tempalteMethod() {
	AbstractTemplate template1 = new AbstractTemplate() {
    	@Override
        protected void call() {
            log.info("비즈니스 로직 실행!");
        }
    };
    
    AbstractTemplate template2 = () -> {
    	log.info("비즈니스 로직 실행!");
    }
    
    template1.execute();
    template2.execute();
}

익명 클래스와 람다식을 활용하면 별도의 클래스를 생성하지 않고 로직 수행이 가능하다!



3. 적용

public abstract class AbstractTemplate<T> {
	
    // 로그 추적 기능을 담당하는 클래스
	private final LogTrace trace;
	
    // 생성자 의존성 주입
    public AbstractTemplate(LogTrace trace) {
    	this.trace = trace;
    }
    
    // 로깅을 위한 Sring Type 인자 message
    public T execute(String message) {
		TraceStatus status = null;
        try {
        	status = trace.begin(message);
        		
            // 로직 호출
            T result = call();
            
            trace.end(status);
            return result;
        } catch (Exception e) {
        	trace.exeception(status, e);
            throw e;
        }
	}
    
    protected abstract T call();
}

먼저 부가 기능의 공통 부분을 추상 템플릿 클래스로 만들어보자.


// OrderController
@GetMapping("/request")
public String request(String itemId) {
	TraceStatus status = null;
    try {
    	status = trace.begin("OrderController.request()");
    	
        orderService.orderItem(itemId); // 핵심 기능
        
        trace.end(status);
        
    } catch (Exception e) {
    	trace.exception(status, e);
        throw e;
    }
    
    return "ok":
}

위 코드의 핵심 기능과 섞여있는 부가 기능을 분리해보자!

...
private final OrderService orderService;
private final LogTrace trace;

// OrderController
@GetMapping("/request")
public String request(String itemId) {
	
	// 1. 익명 클래스
	AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
		@Override
        protected String call() {
        	orderService.orderItem(itemId);
            return "ok";
        }
    };
    
    // 2. 람다식
    AbstractTemplate<String> template = (trace) -> {
        orderService.orderItem(itemId);
        return "ok";
    };


    
    return template.execute("OrderController.request()");
}

보다시피 반복되는 불변하는 부가 기능인 execute()는 상속을 통해 해결하고, 핵심 기능만 구현함으로써 핵심 기능과 부가 기능을 분리할 수 있다.

뿐만 아니라, 부가 기능인 execute()의 변경이 일어나는 경우 AbstractTemplate의 코드만 변경하면 된다. 이는 코드의 감소뿐만 아니라 단일 책임 원칙(SRP)를 준수함으로써 더 좋은 설계가 되었다.

SRP 단일 책임 원칙 (Single Responsibility Principle)

하나의 클래스가 존재할 때, 변경이 있을 때 클래스에 미치는 파급 효과가 적다면 단일 책임의 원칙을 잘 따른 것이다!
클래스의 책임을 적절하게 조절하는것이 중요하다.

ex) 객체의 생성과 사용을 분리한다. 어떠한 요구사항으로 인한 변경이 생길 때 사용을 담당하는 부분만 수정할 수 있도록



템플릿 메서드 패턴의 문제점

템플릿 메서드 디자인 패턴의 목적은 다음과 같습니다.
"작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기합니다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있습니다."

[GOF 디자인 패턴]

부모 클래스에 알고리즘의 골격인 템플릿(변하지 않는 부분)을 정의, 변경되는 로직은 자식 클래스에 정의하는 것이다.

하지만!!!

상속에서 오는 단점이 있다. 이는 부모 클래스와 자식 클래스가 컴파일 시점에 강하게 결합된다는 점이다.

지금까지의 상황으로 보았을 때, 템플릿 메서드 패턴을 위해 자식 클래스는 부모 클래스의 기능을 사용하지 않지만 계속해서 상속을 받고 있었다. (부모 클래스의 추상 메서드를 재정의만 할 뿐..)

이러한 강한 결합때문에 부모 클래스의 변경이 자식 클래스에도 영향을 줄 수 있다.



전략 패턴

강한 결합의 문제가 발생하는 이유는 핵심과 보조 기능을 분리하기 위해 상속을 사용했기 때문이다.

전략 패턴에서는 상속이 아닌 위임으로 문제를 해결한다.

전략 패턴에 관한 내용은 다음 포스팅에서 정리하겠다.





참고문헌
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

0개의 댓글