하나의 스레드에서 메서드간에 TraceId 를 공유하기 위해 LogTrace 의 필드로 TraceId를 두었다. 그런데 LogTrace 는 싱글톤 빈으로 등록하여 사용하므로, 여러 스레드간 동시성 문제가 발생하여 필드를 스레드로컬로 변경해여 해결하였다. 이제 로그추적기 구현에는 문제가 없다. 그런데 로그추적기의 적용 부분을 보자.
컨트롤러의 핵심로직은 빨간박스의 두줄인데 로그추적기 적용을 위해 앞뒤로 매우 지저분한 코드가 추가된다. Service Repository 에도 동일한 코드가 추가된다. 그런데 앞뒤로 코드가 추가되고, 예외처리도 존재해 메서드로 빼기도 어렵다. 그리고 공통 기능이 수정되면 모든 클래스를 수정해야한다.
복잡한 공통 부가기능을 따로 관리하면서 깔끔하게 적용할 수는 없을까?
부모 클래스에 템플릿 메서드와 가변 메서드를 두고, 자식이 가변 메서드를 오버라이딩하는 방식이다. 템플릿 메서드를 호출하는 방식으로 사용한다.
부모 클래스인 AbstractTemplate 이다. 템플릿 메서드인 execute 와 가변 메서드인 call 을 갖는다. call 은 추상 메서드로 자식 클래스에서 오버라이딩하여 사용한다. 템플릿 중간에 call 을 호출하는 것을 볼 수 있다.
자식클래스 생성
자식클래스들이다. 부모클래스를 상속받으며 가변 메서드인 call 을 오버라이딩하여 각자의 가변 기능을 구현한다.
호출
템플릿 + 가변 부분 코드를 호출한다. 오버라이딩한 자식클래스를 인스턴스로 생성하고. 템플릿 메서드를 호출하면된다. 템플릿 메서드 내에서 가변 메서드를 만나면 오버라이딩한 자식 메서드가 호출된다.
자식클래스를 각기 생성한 후 인스턴스로 사용할 수 있다. 그러나 해당 자식클래스를 다른 곳에서 재사용할 것이 아니라면, 익명 클래스를 통해 일회성 자식클래스를 생성하여 사용하는 것이 간단하다. 호출은 동일하게 템플릿 메서드를 호출하면 된다. 부모클래스가 인터페이스가 아니기에 람다식으로는 변경이 불가능하다.
로그 추적기 적용의 공통 부분을 템플릿 메서드인 execute 에 담고, 각 클래스의 비즈니스 로직을 담을 메서드인 call 을 추상메서드로 선언했다. 정상흐름에 영향을 주지 않기 위해 call 의 반환 값을 execute 에서 반환하도록 하였으며, 반환타입이 미정이어서 제네릭으로 선언한 모습이다.
사용할 클래스에서 가변 메서드를 오버라이딩하며 익명클래스를 생성하고, execute 를 실행하면 된다. execute 의 반환 값이 곧 call 의 반환값이기에 이를 그대로 반환하는 모습이다.
제네릭은 객체가 들어가야 한다. 따라서 void 반환 타입은 제네릭을 Void 로 하고 null을 반환하여야 한다.
템플릿 메서드 패턴의 도입을 하였다. 그 결과 부가기능을 AbstractTemplate 클래스의 execute 메서드에서 별도로 관리하게 되었으며 수정도 그곳에서만 하면 되는 장점을 가진다.
하지만 핵심기능에 가변 메서드 오버라이딩과 템플릿 호출 코드가 섞여있으며, 상속을 통해 구현되어있기에 부모 클래스인 AbstractTemplate이 바뀌면 자식클래스에도 영향을 가는 한계가 존재한다.
Context 클래스가 템플릿 메서드를 갖고, 템플릿 메서드 내부에서 Strategy 인터페이스의 call 메서드를 호출한다. Strategy 의 구현체에서 call 메서드를 오버라이딩한다.
즉 템플릿 메서드는 Strategy 인터페이스에 의존. 가변 메서드도 Strategy 인터페이스에 의존하기에. 템플릿 메서드가 변경되어도 가변 메서드에 영향이 가지 않는 장점이 있다.
전략패턴 V1은 Strategy 인터페이스를 Context 클래스의 필드로 갖는 전략패턴이다. Context 객체를 생성하면서 Startegy 구현체를 주입받는다. 이와 같은 Context 의 내부 구현체가 불변일 때 유용하다. 스프링의 빈은 어플리케이션 로딩 시점에 내부 구현체가 주입된 상태로 생성되므로 전략 패턴 V1 이다.
이때 내부 구현체를 변경하려면 set 을 사용하여야 하는데, 외부 객체가 싱글톤이라면 동시성 문제 등 고려할 점이 많다. 그래서 다른 구현체가 주입된 새로운 객체를 생성하는 방식을 채택하는 경우도 있다.
Startegy 인터페이스를 필드로 갖고, 템플릿 메서드인 execute 를 갖는다. 템플릿 메서드 중간에 가변 메서드인 strategy.call 을 호출한다.
Strategy 인터페이스
Strategy 인터페이스이다. 가변메서드인 call 을 갖는다.
Context 의 템플릿 메서드인 execute를 호출하여 사용하는 부분이다.
Strategy 구현 클래스 생성
구현 클래스를 정의 및 인스턴스를 생성하고 Context 생성자에 파라미터로 넘겨 템플릿 메서드를 호출하는 방식이다. 구현 클래스를 일일이 정의해야되는 단점이 있다.
익명 클래스 사용 인스턴스 생성
익명 클래스를 통해 구현 클래스 정의와 인스턴스 생성을 한번에 한다. 생성한 인스턴스를 Context 생성자에 파라미터로 넘겨 템플릿 메서드를 호출한다.
익명 클래스 사용 인스턴스 생성하여 파라미터 전달
Context 생성자의 파라미터에서 익명클래스를 사용한다. 구현 클래스 정의 + 인스턴스 생성 + 파라미터 전달을 한번에 처리한다. Context 객체에서 템플릿 메서드를 호출한다.
람다식 사용
하나의 메서드를 갖는 인터페이스는 람다식을 통해 익명클래스 인스턴스를 생성할 수 있다.
전략패턴 V2는 Context 가 필드로 Strategy 인터페이스를 갖지 않는다. 대신 템플릿 메서드인 execute 를 호출할 때 파라미터로 Startegy 구현체를 주입받는다. Strategy 구현체를 Context 생성 시점이 아닌 execute 호출 시점마다 결정할 수 있다는 장점이 있다.
필드 대신 execute 메서드의 파라미터로 Startegy 를 갖는다.
Context V1 과 같이 자식 클래스 생성하여 파라미터로 주입. 파라미터에서 익명 클래스 생성. 파라미터에서 람다식 사용. 세가지 방식이 있다.
전략패턴 V1은 Context 클래스의 필드로 Strategy 구현체를 주입받는다. 그래서 execute 메서드를 그냥 호출하면 되므로 간편하다. 다만 다른 전략을 사용하려면 다른 Startegy 구현체를 주입받은 새로운 Context 객체를 생성해야한다.
전략패턴 V2는 Context 의 execute 메서드 파라미터로 Strategy 구현체를 주입받는다. 매 execute 호출시마다 Strategy 구현체를 파라미터로 넘겨야하지만, execute 호출시마다 유연하게 전략을 변경할 수 있는 장점이 있다.
어플리케이션 의존 관계에서는 내부 구현체를 미리 정하는 전략패턴 V1 을 사용하지만, 유연하게 내부 구현체를 바꿔야하는 우리 로그추적기에서는 하나의 Context 객체를 사용하게 하는 전략패턴 V2가 더 나은 선택이다.
스프링에서는 전략패턴 V2와 같은 방식을 템플릿 콜백 패턴이라고 한다. 전략 패턴의 Context 가 템플릿 역할을. 파라미터로 넘겨주는 Strategy 가 콜백 역할을 한다. 콜백이란 코드를 넘겨준 곳의 뒤에서 실행된다는 것이다. Startegy 는 파라미터로 execute에 넘어간 뒤(back)에 가변 메서드를 호출(call)한다.
스프링의 JdbcTemplate, RestTemplate 등 xxxTemplate 들은 템플릿 콜백 패턴으로 구현되어있다.
전략패턴의 Context 에 해당하는 Template이다. execute 메서드를 보면 파라미터로 callback 인터페이스를 받고. 중간에 callback.call 메서드를 호출한다. 핵심로직의 결과를 반환하기 위해 call 의 반환형을 제네릭으로 사용한 모습이다.
전략패턴의 Strategy 에 해당하는 Callback 인터페이스이다. call 메서드를 갖는다.
Controller 에 로그추적기를 적용했다. 우선 Template 을 생성하여 필드로 갖는다. 빈으로 Template 을 등록해 주입받아도 된다.
그 후 template.execute 에서 파라미터로 callback 인터페이스의 익명 클래스를 생성하며 호출한다.
Service 와 Repository 에서도 동일한 방식으로 로그추적기를 적용했다. 익명 클래스를 파라미터로 넘겨주는 대신 람다식을 적용하였다.
공통 부가기능을 Template 의 execute 메서드에 별도로 분리하여 관리할 수 있게 되었다. 개별 핵심기능은 Callback 의 call 메서드를 오버라이딩하면서 execute 의 파라미터로 넘겨주면 된다.
다만 템플릿을 사용하는 모든 클래스를 템플릿을 사용하도록 수정해야 한다. 100개의 클래스에 로그추적기를 추가하려면 일일이 템플릿을 사용하도록 고쳐야한다는 것이다.
원본 코드를 손대지 않고 공통 부가기능을 추가할 수 없을까? 이를 위해선 프록시 개념을 이해해야한다.
부모 클래스에 템플릿 메서드, 개별 추상 메서드 두고. 자식에서 개별 추상 메서드를 오버라이딩 하여 사용하는 디자인 패턴.
템플릿을 가진 클래스(Context)가 인터페이스(Strategy)의 메서드를 호출하여 사용하며, 인터페이스의 구현체를 별도로 두는 디자인 패턴. Context 가 필드에 구현체를 주입 받거나, 템플릿 메서드 호출시 파라미터로 전달받을 수 있다.
템플릿 메서드의 파라미터로 구현체를 주입받는 방식의 전략패턴을 스프링에서 부르는 말.
공통 처리인 템플릿 메서드를 따로 빼서 관리할 수 있게 되었지만, 여전히 공통 처리를 위해 모든 클래스에 수정이 필요하다.