Spring/Spring Core Advanced

템플릿 메서드 패턴과 콜백 패턴

민철킹 2021. 11. 1. 22:27

 

2021.10.29 - [Spring/Spring Core Advanced] - ThreadLocal

 

ThreadLocal

2021.10.28 - [Spring/Spring Core Advanced] - 로그 추적기 로그 추적기 애플리케이션이 커질수록 모니터링과 운영이 중요해진다. 어떤 부분에서 병목현상이 발생하는지 어떤 부분에서 예외가 발생하는지

minchul-son.tistory.com

앞서 ThreadLocal을 통해 로그 추적기를 개발하고 애플리케이션에 적용시켜보았다.

 

컨트롤러의 핵심 기능은 서비스를 호출하는 것이다.

 

하지만 로그 추적기를 적용시키면서 로그를 출력하는 부가 기능 코드가 추가되면서 코드가 복잡해졌다.

 

핵심 기능과 부가 기능을 더 효율적으로 변경할 수는 없을까?

 


핵심 기능

핵심 기능해당 객체가 제공하는 고유의 기능이다. 

  • orderService.orderItem()
  • orderRepository.save()

 

부가 기능

핵심 기능보조하기 위한 기능이다. 

 

위의 코드에서는 로그를 출력하기위한 기능이 여기에 해당한다.

 

이러한 기능은 단독으로 사용되기보다는 핵심 기능을 보조하기위해 함께 사용된다.

 


Controller, Service, Repository의 핵심 기능만 다르고 로그를 출력하기 위한 부가기능만 다를뿐인데 이 중복된 코드를 메소드로 뽑아내면 깔끔하게 해결할 수 있을 것 같지만, "try-catch" 블록 내에 존재하고 핵심 기능이 중간에 있어 단순한 추출은 어렵다.

 

 

좋은 설계는 변하는 것과 변하지 않는 것을 분리하는 것이다!

변하는 부분 : 핵심 기능

 

변하지 않는 부분 : 부가 기능(로그 출력)

 

Template Method Pattern이 바로 이런 문제를 해결하는 디자인 패턴이다.

 


Template Method Pattern

템플릿 메소드 패턴을 사용하지 않는 간단한 예제를 살펴보자.

logic1logic2는 비즈니스 로직만 다를 뿐 그 외에 시간을 호출하는 로직은 모두 동일하다.

 

템플릿 메소드 패턴은 기준이 되는 템플릿이라는 틀을 사용하는 방법이다.

 

템플릿이라는 틀에 변하지 않는 부분을 몰아두고 변하는 부분별도로 Override하여 호출하는 식으로 해결한다.

 

추상 클래스를 통해 정의할 수 있는데 변하지 않는 부분을 몰아넣고 변하는 부분은 자식 클래스에서 상속을 하여 개별적으로 처리할 수 있도록 한다.

 

자식 클래스

템플릿을 상속받는 자식 클래스를 만들어 각자의 비즈니스 로직을 여기 안에서 구현하면 된다.

 

위와 같이 훨씬 더 간단하고 유연한 방법으로 코드를 변경할 수 있게된다.

 


이번에는 익명 내부 클래스를 사용하여 템플릿 메소드 패턴을 사용해보자.

 

매번 추상 클래스를 상속받는 자식 클래스를 생성하는 번거로움을 해결할 수 있다.


앞서 만든 로그 추적기를 템플릿 메소드 패턴을 사용해 애플리케이션에 적용시켜보자.

먼저 각 레이어에서 상속받을 추상 클래스를 정의한다.

 

LogTrace를 사용하기 때문에 생성자 주입을 통해 주입을 받는다.

 

또한 제네릭 타입을 사용해 반환타입을 생성 시점에 결정하도록 위임하였다.

 

이제 이를 각 레이어에서 call을 재정의하여 각자의 비즈니스 로직만 재정의해주면 된다.

 

복잡한 try-catch를 사용하던 이전 코드를 위와 같이 변경할 수 있다.

 

객체를 생성하며 상속받은 자식 클래스를 정의하였다.

 

익명 내부 클래스를 사용했지만 상속받는 자식 클래스를 만들어 내부 구현을 숨기는 방식으로도 사용할 수 있다.

 

 


좋은 설계

좋은 설계는 변경이 일어날 때 자연스럽게 드러난다.

 

만약 위의 코드에서 로그 추적기의 로직을 변경해야한다면 추상 클래스인 AbstractTemplate만 변경하면 된다.

 

만약 템플릿 메소드 패턴을 사용하지 않았다면 각 레이어에 있는 모든 로그 출력 로직을 일일이 변경해야할 것이다.

 

이는 단일 책임 원칙을 준수하는 것으로 변경에 손쉽게 대처할 수 있게 된다.

 


템플릿 메소드 패턴의 정의

부모 클래스에 골격인 템플릿을 정의하고 변경 로직을 자식 클래스에 정의하는 것이다.

 

자식 클래스는 전체 구조를 변경하지 않고 특정 부분만 재정의하여 유연하게 사용할 수 있게된다.

 

하지만 여기에는 단점도 존재한다!

 

기본적으로 상속을 사용하기 때문에 상속의 단점(캡슐화 깨트림 등)이 그대로 발생한다.

 

자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되기 때문에 의존관계에 대한 문제가 있을 수 있다.

 

자식 클래스가 부모 클래스의 메소드를 사용하든 그렇지 않든 부모 클래스를 강하게 의존한다.

 

사용하지 않더라도 부모 클래스를 알고 있어야 하기 때문에 때로는 좋은 설계이지 않을 수 있다.

 

그렇기 때문에 부모 클래스가 수정된다면 모두 자식 클래스에도 영향을 줄 수 있다.

 

또한, 별도의 클래스나 익명 내부 클래스를 만들어야하는 부분도 매끄럽지 않은 부분이다.

 

템플릿 메소드 패턴과 비슷하면서 상속의 단점을 제거하는 것이 바로 전략 패턴이다!!

 


Strategy Pattern

전략 패턴은 변하지 않는 부분을 Context 라는 곳에 두고 변하는 부분을 Strategy라는 인터페이스를 만들어 이를 구현하도록 하여 문제를 해결한다. 

 

즉, 상속이 아닌 위임으로 문제를 해결한다.

 

이를 통해 각각을 캡슐화해 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.

 

변하는 것인 각 비즈니스 로직을 인터페이스를 통해 만들어놓고 사용하는 곳에서 각각 이것을 구현한다.

 

Context

Context는 필드로써 Strategy를 가지고 있고 이를 생성자 주입을 통해 주입받는다.

 

그리고 execute를 통해 Strategy 인스턴스에서 call()을 위임하고 있으므로 매우 유연하게 사용할 수 있다.

 

이는 스프링을 사용한 개발자라면 매우 친숙한 방법인데 바로 스프링 의존관계를 주입받는 것과 동일하다.

이는 원하는 구현체를 갈아끼울 수 있어 매우 유연하게 사용가능하다는 장점이 존재한다.

 

 

상속하는 것이 아닌 단순한 인터페이스를 구현하고 있기 때문에 Context가 바뀌더라도 영향을 전혀 받지 않는다!!

 


전략 패턴 또한 익명 내부 클래스를 사용할 수 있다.

 

Lambda와 함께 사용한다면 아래와 같이 더 깔끔하게 변경이 가능하다.

  • 대신 Strategy 인터페이스에 메소드가 1개만 존재해야함

 

이 방식은 로딩 시점에 한번 Context와 Strategy를 조립하고 그 이후에는 실행만 하면 된다.

 

하지만 단점은 ContextStrategy를 한번 조립한 이후에는 전략을 변경하기가 번거롭다.

  • Context에 setter를 제공해 Strategy를 넘겨받고 변경할 수 있지만 Context가 싱글톤으로 사용될 때는 동시성 이슈 등 고려해야할 점이 많다.

 

이 단점을 해결하기 위해 전략 패턴을 조금 다르게 사용해보자.

 

전략을 실행할 때 직접 파라미터로 전달해 사용하는 것이다.

실행될 때마다 파라미터로 전달받는 방식으로 Context를 변경하였다.

선 조립 후 실행이 아닌 실행할 때마다 원하는 Strategy를 넘겨줄 수 있다.

 

따라서 원하는 전략을 더욱 유연하게 변경할 수 있다.

 

각 상황에 맞게 사용하자!(선 조립 후 실행, 실행 시점에 전달)

 


Template Callback Pattern

위의 ContextV2에서는 변하는 부분인 Strategy를 파라미터로 넘겨줘 처리하도록 하였다.

 

이렇게 다른 코드의 인수로써 넘겨주는 실행 가능한 코드를 Callback이라고 한다.

 

CallBack 또는 Call-After Function은 다른 코드의 인수로써 넘겨주는 실행 가능한 코드를 말한다.

콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할수도, 나중에 실행할 수도 있다.

즉, Strategy를 넘겨주고 실행은 클라이언트가 아닌 Context에서 실행되는 것이다.

 

자바에서는 실행 가능한 코드, 콜백을 사용하기 위해서는 객체를 넘겨야하기 때문에, 람다를 사용하는 방식으로 처리한다.

 


Spring에서 ContextV2와 같은 방식의 Strategy PatternTemplate Callback Pattern이라고 한다.

context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());

Template Callback PatternGoF 디자인 패턴이 아닌 Spring 내부에서 자주 사용하는 방식으로써 Spring 내에서만 이렇게 부른다.

 

xxxTemplate라는 클래스가 존재한다면 Tempate Callback Pattern을 사용하는 것으로 생각하면된다.

  • 최근에 세션 클러스터링을 진행하면서 사용한 RedisTemplate!!!

 


애플리케이션에 Template Callback Pattern을 적용시켜보자.

먼저, Callback 인터페이스를 정의한다.

그 뒤에 템플릿 클래스를 정의한다.

 

TraceTemplate 인스턴스의 execute()가 실행될 때 Callback 인터페이스구현체를 넘겨받아 그 구현체의 call()을 호출한다.

 

해당 클래스 자체가 싱글톤이기 때문에 위와 같이 생성자 주입을 진행하면서 new를 사용해 TraceTemplate를 생성하고 LogTrace를 주입해주는 방법을 사용하였다.

 

이렇게하지 않고 @RequiredArgsConstructor를 사용하고 TraceTemplate 자체를 스프링 빈으로 등록하여 사용해도됨!

 

TraceTemplate 인스턴스의 execute를 호출하면서 익명 클래스를 통해 콜백을 넘겨주었다.

 

이것이 바로 Template Callback pattern이다!

 

람다 사용

 

람다식을 사용한다면 더 깔끔하게 코드를 변경할 수 있다.

 

 

 

반응형

'Spring > Spring Core Advanced' 카테고리의 다른 글

Spring의 프록시 기술  (0) 2021.12.16
동적 프록시 기술  (0) 2021.11.18
프록시 패턴과 데코레이터 패턴  (0) 2021.11.08
ThreadLocal  (0) 2021.10.29
로그 추적기  (0) 2021.10.28