[김영한 스프링 review] 스프링 핵심 원리 - 고급편 (2)

조갱·2024년 4월 6일
0

스프링 강의

목록 보기
13/16

시작하기 전에

이전 포스팅의 마무리에서 언급했듯, 지금부터는 공통 기능을 디자인 패턴을 사용하여 해결한다.
이 포스팅을 보기 위해 반드시 템플릿 메소드 패턴, 전략 패턴, 템플릿 콜백 패턴 포스팅을 이해하고 오자.

v4 - 템플릿 메소드 패턴

v4 에서 템플릿 메소드 패턴을 적용하기 위해, AbstractTemplate을 먼저 정의하자.

Abstract Template

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

이전 포스팅의 마무리에서 언급한 공통 관심사인 로그를 찍는 로직을 템플릿화 했다.

Controller V4

@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<>(trace) {
			@Override
			protected String call() {
				orderService.orderItem(itemId);
				return "ok";
			}
		};

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

Service V4

@Service
@RequiredArgsConstructor
public class OrderServiceV4 {
	private final OrderRepositoryV4 orderRepository;
	private final LogTrace trace;

	public void orderItem(String itemId) {
		AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
			@Override
			protected Void call() {
				orderRepository.save(itemId);
				return null;
			}
		};
		template.execute("OrderService.orderItem()");
	}
}

Repository V4

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV4 {
	private final LogTrace trace;

	public void save(String itemId) throws Exception {
		AbstractTemplate<Void> template = new AbstractTemplate<>(trace) {
			@Override
			protected Void call() {
				//저장 로직
				if (itemId.equals("ex")) {
					throw new IllegalStateException("예외 발생!");
				}
				Thread.sleep(1000);
				return null;
			}
		};
		template.execute("OrderRepository.save()");
	}
}

V3 -> V4 Diff 보기
코드의 라인수가 줄지는 않았지만,
적어도 공통 관심사를 Template화 시킴으로서 로직 내부에서 로그를 찍는 코드를 제거했다.
(= 핵심 로직에 집중하게 됐다.)

하지만, 템플릿 메소드 패턴의 단점은 템플릿 메소드 패턴, 전략 패턴, 템플릿 콜백 패턴 포스팅에서도 볼 수 있듯, 상속의 단점을 그대로 앉고 간다는 것이다.

이를 전략 패턴을 사용한, (스프링에서만 그렇게 불리우는) 템플릿 콜백 패턴으로 다시 풀어보자.

v5 - 템플릿 콜백 패턴

TraceCallback 인터페이스

전략 패턴(템플릿 콜백 패턴)에서는 인터페이스를 상속받기 때문에, 콜백을 처리하기 위한 인터페이스를 정의한다.

public interface TraceCallback<T> {
	T call();
}

TraceTemplate

공통 기능(로그 출력)을 모아둔 템플릿이다. 템플릿 메소드 패턴의 템플릿과 같다.
다만, 차이점이라면 전략 패턴(템플릿 콜백 패턴)에서는 인터페이스를 받기 때문에 execute 메소드의 2번째 인자로 TraceCallback 인터페이스를 받아서 실행한다.

public class TraceTemplate {
	private final LogTrace trace;

	public TraceTemplate(LogTrace trace) {
		this.trace = trace;
	}

	public <T> T execute(String message, TraceCallback<T> callback) {
		TraceStatus status = null;
		try {
			status = trace.begin(message);
			//로직 호출
			T result = callback.call();
			trace.end(status);
			return result;
		} catch (Exception e) {
			trace.exception(status, e);
			throw e;
		}
	}
}

Controller V5

@RestController
@RequiredArgsConstructor
public class OrderControllerV5 {
	private final OrderServiceV5 orderService;
	private final TraceTemplate template;

	@GetMapping("/v5/request")
	public String request(String itemId) {
		return template.execute("OrderController.request()", new TraceCallback<>() {
			@Override
			public String call() {
				orderService.orderItem(itemId);
				return "ok";
			}
		});
	}
}

Service V5

@Service
@RequiredArgsConstructor
public class OrderServiceV5 {
	private final OrderRepositoryV5 orderRepository;
	private final TraceTemplate template;

	public void orderItem(String itemId) {
		template.execute("OrderService.orderItem()", () -> {
			orderRepository.save(itemId);
			return null;
		});
	}
}

Repository V5

@Repository
@RequiredArgsConstructor
public class OrderRepositoryV5 {
	private final TraceTemplate template;

	public void save(String itemId) throws Exception {
		template.execute("OrderRepository.save()", () -> {
			//저장 로직
			if (itemId.equals("ex")) {
				throw new IllegalStateException("예외 발생!");
			}
			Thread.sleep(1000);
			return null;
		});
	}
}

V4 -> V5 Diff 보기

인터페이스는 메소드가 1개만 존재하면, 람다로 오버라이드를 할 수 있다.
그래서 템플릿 메소드 패턴에서 @Override 하던 코드가 사라졌음을 알 수 있다.
-> 코드가 조금 더 깔끔해졌다.

마무리

이제 진짜 로그찍기 시리즈가 끝이 났다. 👏
V0 -> V5까지 개선하면서 무지성으로 구현했을 때랑 디자인 패턴을 사용했을 때의 차이점을 명확히 느꼈을 것이라 생각한다.

V0 -> V5 Diff 보기
V0 -> V5 로 변화된 모습을 보면 여전히 부가적인 코드(템플릿) 때문에 지저분해보이지만, 그래도 핵심기능에 더 집중했음을 알 수 있다.

혹시 잘 느껴지지 않는다면, V1 -> V5 Diff 보기 를 통해 리팩토링이 잘 됐음을 느껴보자.

한계

하지만 한계점도 분명히 존재하는데, 아무리 리팩토링을 하더라도
기존 로직의 수정 없이는 개선할 수 없다는 점이다.

이는 이후에 다룰 Spring AOP를 통해 해결할 수 있지만, AOP를 이해하기 위해서는 또 다른 개념 (Proxy) 를 알아야한다.

다음 시간에는 Proxy에 대해 살펴보자.

profile
A fast learner.

0개의 댓글