스프링 핵심 원리 - 고급편을 학습하며 정리한 내용입니다.
-- 정상 흐름 --
[459dca9c] OrderController.request()
[459dca9c] |-->OrderService.orderItem()
[459dca9c] | |-->OrderRepository.save()
[459dca9c] | |<--OrderRepository.save() time=1004ms
[459dca9c] |<--OrderService.orderItem() time=1007ms
[459dca9c] OrderController.request() time=1010ms
-- 예외 발생 --
[2e21f1af] OrderController.request()
[2e21f1af] |-->OrderService.orderItem()
[2e21f1af] | |-->OrderRepository.save()
[2e21f1af] | |<X-OrderRepository.save() time=1ms ex=java.lang.IllegalStateException: 예외 발생!
[2e21f1af] |<X-OrderService.orderItem() time=1ms ex=java.lang.IllegalStateException: 예외 발생!
[2e21f1af] OrderController.request() time=1ms ex=java.lang.IllegalStateException: 예외 발생!
// V0 Service 일부
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
//V3 Service 일부
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;
}
}
핵심 기능보다 로그를 출력해야 하는 부가 기능 코드가 훨씬 더 많고 복잡하다.
핵심 로직 별 로그를 다는 보조 기능을 추가한 것 뿐인데, 배보다 배꼽이 커졌다!
변하는 것과 변하지 않는 것을 분리하자
// V3 Controller
@GetMapping("/v3/request")
public String request(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderController.request()");
orderService.orderItem(itemId); // 핵심 로직
trace.end(status);
return "ok";
} catch (Exception e) {
trace.exception(status, e);
throw e; //예외를 꼭 다시 던져주어야 한다.
}
}
// V3 Service
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;
}
}
// V3 Repository
public void save(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderRepository.save()");
if (itemId.equals("ex")) { // 핵심로직
throw new IllegalStateException("예외 발생!");
}
sleep(1000); // 핵심로직
trace.end(status);
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
공통적으로 로그 남기는 부분 때문에 코드가 구구절절 길어지는데 막상 핵심 로직은 일부분이다.
변하지 않는 부분을 분리시켜서 모듈화 하자!
=> 템플릿 메소드 패턴 도입하기
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(); // 구체 클래스에 핵심 로직의 구현 위임
}
결과적으로 비즈니스 로직만 남기도록 구현할 수 있다 (익명 내부 클래스 활용)
// V4 Controller
@GetMapping("/v4/request")
public String request(String itemId) {
AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
@Override
protected String call() {
orderService.orderItem(itemId);
return "ok";
}
};
return template.execute("OrderController.request()");
}
// V4 Service
public void orderItem(String itemId) {
AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
@Override
protected Void call() {
orderRepository.save(itemId);
return null;
}
};
template.execute("OrderService.orderItem()");
}
// V4 Repository
public void save(String itemId) {
AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
@Override
protected Void call() {
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
return null;
}
};
template.execute("OrderRepository.save()");
}
좋은 설계는 변경이 일어날 때 드러난다. 로그 남기는 로직을 수정하는 상황을 생각해보자.
변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조가 되었다. 결과적으로 단일 책임 원칙 (SRP)을 잘 지킨 설계라고 할 수 있다.
작업에서 알고리즘의 기본 골격을 정의하고, 일부 단계를 하위 클래스로 연기하여 구체적인 실행 동작을 하위 클래스에 위임하는 방식이다.
상속의 문제점을 안고 간다:
부모-자식 클래스가 컴파일 시점에 강결합 되고, 자식 클래스는 부모 클래스에 강하게 의존한다는 단점이 있다.
(-> 따지고보면 자식 클래스는 부모 클래스의 기능을 사용하지 않지만 다 상속받는다)
✅ 자식 클래스는 부모 클래스에 강하게 의존한다는 말은, 자식 클래스의 코드에 부모 클래스 코드가 적혀 있다는 것
이 때문에 부모 클래스에서 뭔가 바뀌면 자식 클래스는 영향을 받을 수밖에 없다 (side effect 발생)