스프링 핵심 원리 - 고급편의 복습을 위한 글이며,
이 글에 나오는 모든 사진과 코드의 저작권은 김영한 강사님께 있습니다.
1. 템플릿 메소드 패턴
로그 추적기 도입 전과 후의 코드를 비교해보자.
로그 추적기 도입 전 - V0
//OrderControllerV0 코드
@GetMapping("/v0/request")
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
로그 추적기 도입 후 - V3
//OrderControllerV3 코드
@GetMapping("/v3/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";
}
//OrderServiceV3 코드
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderService.orderItem()");
orderRepository.save(itemId); //핵심 기능
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
V0는 메소드가 실제 처리해야 하는 핵심 기능만 깔끔하게 남아있다. 반면에 V3는 핵심 기능보다 로그를 출력해야하는 부가 기능 코드가 훨씬 더 많고 복잡하다.
- 핵심 기능 : 객체가 제공하는 고유의 기능이다. 예를 들어 OrderService의 핵심 기능은 주문 로직이다.
- 부가 기능 : 핵심 기능을 보조하기 위해 제공되는 기능이다. 로그 추적 로직, 트랜잭션 기능 등이 이에 해당한다.
좋은 설계란 변하는 것과 변하지 않는 것을 분리하는 것이다. 여기서 핵심 기능 부분은 변하고, 로그 추적기를 사용하는 부분은 변하지 않는 부분이다. 이 둘을 분리해서 모듈화 해야 한다.
여기서 사용하는 해결 방법이 템플릿 메소드 패턴이다. 템플릿 메서드 패턴은 객체지향 디자인 패턴 중 하나로, 기능의 뼈대(템플릿)와 실제 구현을 분리하는 패턴을 의미한다.
1.1 템플릿 메소드 패턴 - 예제 1
TemplateMethodTest
logic1()
과 logic2()
를 호출하는 단순한 테스트 코드이다. logic1()
과 logic2()
는 시간을 측정하는 로직과 비즈니스 로직을 실행하는 부분이 함께 존재한다.
- 변하는 부분 : 비즈니스 로직
- 변하지 않는 부분 : 시간을 측정하는 로직
@Slf4j
public class TemplateMethodTest {
@Test
void templateMethodV0() {
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
1.2 템플릿 메소드 패턴 - 예제 2
템플릿 메소드 패턴은 이름 그대로 템플릿을 사용하는 방식이다. 템플릿은 기준이 되는 틀이다. 템플릿이라는 틀에 변하지 않는 코드들을 몰아주는 것이다. 그리고 일부 변하는 부분을 별도로 호출해서 해결한다.
AbstractTemplate
변하지 않는 시간 측정 로직만 몰아둔 코드이다. 템플릿 안에서 변하는 부분은 call()
메소드를 호출하여 처리한다. 변하는 부분은 자식 클래스로 두고 상속과 오버리이딩을 사용해서 처리해야 한다.
👉 abstract vs interface
abstract : 부모의 기능을 자식에서 확장시켜나가고 싶을 때
interface : 해당 클래스가 가진 함수의 기능을 활용하고 싶을 때
@Slf4j
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
call();
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
protected abstract void call();
}
SubClassLogic1
@Slf4j
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
}
SubClassLogic2
@Slf4j
public class SubClassLogic2 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
}
TemplateMethodTest - V1 추가
/**
* 템플릿 메소드 적용
*/
@Test
void templateMethodV1() {
AbstractTemplate template1 = new SubClassLogic1();
template1.execute();
AbstractTemplate template2 = new SubClassLogic2();
template2.execute();
}
1.3 템플릿 메소드 패턴 - 예제 3
템플릿 메소드 패턴은 SubClassLogic1, SubClassLogic2 처럼 클래스를 계속 만들어야 하는 단점이 있다. 익명 내부 클래스를 사용하면 객체 인스턴스를 생성하면서 동시에 생성할 클래스를 상속 받은 자식 클래스를 정의할 수 있다. 직접 지정하는 이름이 없고 클래스 내부에 선언되는 클래스여서 익명 내부 클래스라 한다.
TemplateMethodTest - V2 추가
@Test
void templateMethodV2() {
AbstractTemplate template1 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
};
log.info("클래스 이름1={}", template1.getClass());
template1.execute();
AbstractTemplate template2 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
};
log.info("클래스 이름2={}", template2.getClass());
template2.execute();
}
1.4 템플릿 메소드 패턴 적용 - V4
여태 만든 애플리케이션의 로그 추적기에 템플릿 메소드 패턴을 적용해보겠다.
AbstractTemplate
템플릿 메소드 패턴에서의 부모 클래스이고, 템플릿 역할을 한다. <T> 제네릭을 사용해 반환 타입을 정의하였다.
👉 제네릭에 대한 자세한 내용은 여기를 참고해주세요.
아래 코드가 변하는 부분을 처리하는 메소드이다. 이 부분을 상속으로 구현해야 한다.
protected abstract T call();
다음은 코드 전문이다.
public abstract class AbstractTemplate<T> {
private final LogTrace trace;
public AbstractTemplate(LogTrace trace) {
this.trace = trace;
}
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.exception(status, e);
throw e;
}
}
protected abstract T call();
}
OrderControllerV4
AbstractTemplate<String>
으로 선언했기 때문에 AbstractTemplate
의 반환 타입은 String이 된다. 익명 내부 클래스를 사용해 객체를 생성하면서 상속 받은 자식 클래스까지 한번에 정의하였다.
@RestController
@RequiredArgsConstructor
public class OrderControllerV4 {
private final OrderServiceV4 orderService;
private final LogTrace trace;
@GetMapping("/v4/request")
public String request(String itemId) {
AbstractTemplate<String> template = new AbstractTemplate<String>(trace) {
@Override
protected String call() {
orderService.orderItem(itemId);
return "ok";
}
};
return template.execute("OrderController.request()");
}
}
OrderServiceV4
반환할 내용이 없으면 AbstractTemplate<Void>
으로 선언해서 null을 반환하면 된다.
@Service
@RequiredArgsConstructor
public class OrderServiceV4 {
private final OrderRepositoryV4 orderRepository;
private final LogTrace trace;
public void orderItem(String itemId) {
AbstractTemplate<Void> template = new AbstractTemplate<Void>(trace) {
@Override
protected Void call() {
orderRepository.save(itemId);
return null;
}
};
template.execute("OrderService.request()");
}
}
OrderRepositoryV4
@Repository
@RequiredArgsConstructor
public class OrderRepositoryV4 {
private final LogTrace trace;
public void save(String itemId) {
AbstractTemplate<Void> template = new AbstractTemplate<Void>(trace) {
@Override
protected Void call() {
// 저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
return null;
}
};
template.execute("OrderRepository.save()");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
http://localhost:8080/v4/request?itemId=hello 와 http://localhost:8080/v4/request?itemId=ex로 접속하여 정상적으로 작동하는지 확인하자.
1.5 템플릿 메소드 패턴 정리
템플릿 메소드 패턴이란 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의하는 것이다. 이렇게 하면 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있다. 결국 상속과 오버라이딩을 통한 다형성으로 문제를 해결하는 것이다.
그러나, 템플릿 메서드 패턴은 상속을 사용한다. 부모 클래스에 강하게 의존하고 있다는 것과, 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는데, 부모 클래스를 알아야 하는 등의 상속의 단점을 다 안고 간다.
2. 전략 패턴
템플릿 메소드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴이 전략 패턴(Strategy Pattern)이다. 먼저 동일한 문제를 전략 패턴으로 풀어보자.
ContextV1Test
@Slf4j
public class ContextV1Test {
@Test
void strategyV0() {
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
2.1 전략 패턴 - 예제 1
템플릿 메소드 패턴은 부모 클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두어서 상속을 사용해 문제를 해결했다. 전략 패턴은 변하지 않는 부분을 Context
라는 부분에 두고, 변하는 부분을 Strategy
라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다.
Strategy
변하는 알고리즘 역할을 하는 인터페이스이다.
public interface Strategy {
void call();
}
StrategyLogic1
@Slf4j
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
StrategyLogic1
@Slf4j
public class StrategyLogic2 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
}
ContextV1
ContextV1
은 변하지 않는 로직을 갖고 있는 템플릿 역할을 하는 코드이다. 전략 패턴에서는 이것을 컨텍스트(문맥)이라 한다. 코드를 보면 스프링에서 사용하는 방식과 비슷하다. 스프링에서 의존관계 주입에서 사용하는 방식이 전략 패턴이다.
/**
* 필드에 전략을 보관하는 방식
*/
@Slf4j
public class ContextV1 {
private Strategy strategy;
public ContextV1(Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
strategy.call();
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
ContextV1Test - 전략 패턴 추가
아래 메소드를 보면, 의존관계 주입을 통해 ContextV1
에 Strategy
의 구현체인 strategyLogic1
을 주입하는 것을 확인할 수 있다. 이런 식으로 Context
안에 원하는 전략을 주입한다.
@Slf4j
public class ContextV1Test {
/**
* 전략 패턴 사용
*/
@Test
void strategyV1() {
StrategyLogic1 strategyLogic1 = new StrategyLogic1();
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
StrategyLogic2 strategyLogic2 = new StrategyLogic2();
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
}
ContextV1Test - 추가
전략 패턴도 당연하게 익명 내부 클래스를 사용할 수 있다.
@Slf4j
public class ContextV1Test {
/**
*전략 패턴 익명 내부 클래스1
*/
@Test
void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
};
ContextV1 context1 = new ContextV1(strategyLogic1);
log.info("strategyLogic1={}", strategyLogic1.getClass());
context1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
};
ContextV1 context2 = new ContextV1(strategyLogic2);
log.info("strategyLogic2={}", strategyLogic2.getClass());
context2.execute();
}
}
ContextV1Test - 추가
익명 내부 클래스에 변수를 담아두지 않고, 생성하면서 바로 전달해도 된다.
@Slf4j
public class ContextV1Test {
/**
*전략 패턴 익명 내부 클래스2
*/
@Test
void strategyV3() {
ContextV1 context1 = new ContextV1( new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context1.execute();
ContextV1 context2 = new ContextV1(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context2.execute();
}
}
ContextV1Test - 추가
람다식을 사용하면 더 깔끔하게 리팩토링 할 수 있다.
@Slf4j
public class ContextV1Test {
/**
* 전략 패턴, 람다
*/
@Test
void strategyV4() {
ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
context1.execute();
ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
context2.execute();
}
}
위 예제들에서 사용한 방법은 스프링으로 애플리케이션을 개발할 때 애플리케이션 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고 난 다음에 실제 요청을 처리하는 것과 같은 원리이다.
Context
와 Strategy
를 실행 전에 원하는 모양으로 조립해두고, 그 다음에 Context
를 실행하는 선 조립, 후 실행 방식이다. 그러나, Context
와 Strategy
를 조립한 이후에는 전략을 변경하기 어렵다는 단점이 있다.
2.2 전략 패턴 - 예제 2
위 방식보다 좀 더 유연하게 전략 패턴을 사용해보겠다. 이번에는 전략을 실행할 때 직접 파라미터로 전달해서 사용하겠다.
ContextV2
V1과는 다르게 전략을 필드로 갖지 않는다. 대신 execute( ... )
가 실행될 때 마다 전략을 파라미터로 받는다.
/**
* 전략을 파라미터로 전달 받는 방식
*/
@Slf4j
public class ContextV2 {
public void execute(Strategy strategy) {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
strategy.call();
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
ContextV2Test
선 조립 후 실행하는 방식이 아니라, Context
를 실행할 때 마다 전략을 인수로 전달한다. 하나의 Context
에 실행 시점에 여러 전략을 인수로 전달해서 유연하게 실행 할 수 있다는 것이 핵심이다.
@Slf4j
public class ContextV2Test {
/**
* 전략 패턴 적용
*/
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
}
ContextV2Test - 추가
@Slf4j
public class ContextV2Test {
/**
* 전략 패턴 익명 내부 클래스
*/
@Test
void strategyV2() {
ContextV2 context = new ContextV2();
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
}
}
ContextV2Test - 추가
@Slf4j
public class ContextV2Test {
/**
* 전략 패턴 익명 내부 클래스2, 람다
*/
@Test
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직1 실행"));
context.execute(() -> log.info("비즈니스 로직2 실행"));
}
}
2.3 전략 패턴 정리
ContextV1
는 필드에 Strategy
를 저장하는 방식으로 전략 패턴을 구사했다.
- 선 조립, 후 실행 방식에 적합하다.
Context
를 실행하는 시점에는 이미 조립이 끝났기 때문에 전략을 신경쓰지 않고 단순히 실행만 하면 된다.
ContextV2
는 파라미터에 Strategy
를 전달 받는 방식으로 전략 패턴을 구사했다.
- 실행할 때 마다 전략을 유연하게 변경할 수 있다.
- 단점 역시 실행할 때 마다 전략을 계속 지정해야 한다.
Reference
'Backend > Spring - Core' 카테고리의 다른 글
Spring Advanced #11. 스프링 AOP 포인트컷 (0) | 2023.03.13 |
---|---|
Spring Advanced #3. 템플릿 메소드 패턴과 콜백 패턴 (2) (0) | 2023.03.09 |
Spring Advanced #2. 쓰레드 로컬 - ThreadLocal (0) | 2023.03.09 |
Spring Advanced #1. 로그 추적기 (2) (0) | 2023.03.09 |
Spring Advanced #1. 로그 추적기 (1) (0) | 2023.03.07 |