[스프링 핵심 원리 고급편 정리] 템플릿 메소드 패턴

개발새발log·2022년 12월 22일
0

Java/Spring

목록 보기
1/6

스프링 핵심 원리 - 고급편을 학습하며 정리한 내용입니다.

로그추적기 프로젝트

-- 정상 흐름 --
[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 ~ V3의 흐름 복습

V0: 핵심 비즈니스 로직만

V1: 동기화 적용하지 않은 버전 (id 다르게 적용되는 문제)

V2: 동기화 적용한 버전 (-> 파라메터로 id 넘겨줌)

V3: threadlocal 활용

여기까지 문제는?

// 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()");
}

템플릿 메소드 패턴

좋은 설계인가 ?

좋은 설계는 변경이 일어날 때 드러난다. 로그 남기는 로직을 수정하는 상황을 생각해보자.

  • V3의 경우, 로그 남기는 로직을 고치려면 모든 클래스의 로직을 수정해야 한다
  • 반면, V4에서는 템플릿 코드만 손보면 된다

변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조가 되었다. 결과적으로 단일 책임 원칙 (SRP)을 잘 지킨 설계라고 할 수 있다.

개념

작업에서 알고리즘의 기본 골격을 정의하고, 일부 단계를 하위 클래스로 연기하여 구체적인 실행 동작을 하위 클래스에 위임하는 방식이다.

  • 핵심 : 상속과 오버라이딩을 통한 다형성으로 문제를 해결한다

문제는 ..

상속의 문제점을 안고 간다:
부모-자식 클래스가 컴파일 시점에 강결합 되고, 자식 클래스는 부모 클래스에 강하게 의존한다는 단점이 있다.
(-> 따지고보면 자식 클래스는 부모 클래스의 기능을 사용하지 않지만 다 상속받는다)

자식 클래스는 부모 클래스에 강하게 의존한다는 말은, 자식 클래스의 코드에 부모 클래스 코드가 적혀 있다는 것

이 때문에 부모 클래스에서 뭔가 바뀌면 자식 클래스는 영향을 받을 수밖에 없다 (side effect 발생)

profile
⚠️ 주인장의 머릿속을 닮아 두서 없음 주의 ⚠️

0개의 댓글