이전 포스팅에서 템플릿 메서드 패턴을 통해서 비지니스로직과 부가기능인 로깅을 분리했었다. 하지만 상속을 사용해서 결합도가 증가하고, 추상 내포클래스를 사용해서 가독성이 떨어지는 등의 문제가 있었다.
이런 문제들을 해결하기 위해 콜백을 사용한 전략패턴을 통해서 로깅부분을 수정 해 보자
템플릿 메서드 패턴에서 부모클래스를 추상클래스로 만들고 상속을 통해서 call 메서드를 구현했었다.
전략 패턴에서도 call 메서드를 다형적으로 사용하는 것에는 변함이 없지만 다형성을 상속을 통해서 해결하는 것이 아닌 컴포지션을 통해서 해결한다.
우선 전략에 해당하는 ILogStrategy 인터페이스를 만들어주고 call메서드의 선언부를 넣어준다.
public interface ILogStrategy<T> {
T call();
}
이제 LogContext 클래스에서 ILogStrategy의 구현체를 넘겨받아서 실행한다
@Component
@RequiredArgsConstructor
public class LogContext {
public <T> T execute(String message, ILogStrategy<T> strategy) {
Long startTimeMs = System.currentTimeMillis(); // 시작
strategy.call();
Long endTimeMs = System.currentTimeMillis(); // 끝
log.info("time - {}", endTimeMs - startTimeMs); // 걸린 시간
}
}
컨텍스트 클래스의 execute 메서드에서 곧바로 strategy 인터페이스의 구현을 넘겨받아서 실행한다.
이제 호출하는 부분을 보자
@PostMapping("/postings/posting")
public ResponseEntity createPosting(@RequestBody @Valid CreatePostingRequest body) {
logger.execute("PostingController.createPosting", () -> {
return postingService.createPosting(body.getTitle(), body.getContent());
});
return new ResponseEntity<>(null, null, HttpStatus.CREATED);
}
컨텍스트 클래스의 execute 메서드에 메세지와 call함수의 구현체를 함께 넘긴것을 확인할 수 있다.
주목할점은 execute함수의 두번째 파라미터는 인터페이스인데 호출하는 부분에서는 인자로 함수의 구현체를 넘긴것이다.
이것은 Java 8부터 메서드가 하나인 인터페이스의 경우 구현체만 넘겨도 상관없도록 자바에서 synthetic sugar를 제공하기 때문이다. C에서의 함수포인터처럼 함수의 주소를 넘기는 방식으로 구현체를 실행할 것으로 예상된다.
템플릿 메서드 패턴의 경우 전략 패턴에서의 컨텍스트 클래스에 해당하는 부분을 추상클래스로 만들어주고, Strategy에 해당하는 구체클래스가 추상클래스를 상속받아서 call 메서드를 구현해주는 방식을 택했었다. 그래서 구체클래스가 부모클래스에 직접적으로 의존하는 상황이 발생했고, 구체클래스는 부모클래스의 수정에 직접적으로 연관을 받았기 때문에 유지보수성이 떨어졌다.
하지만 전략패턴에서는 컨텍스트 클래스가 Strategy 인터페이스(ILogStrategy
)에 의존하기 때문에 ILogStrategy
인터페이스를 구현하는 구현체들은 컨텍스트 클래스의 변경에 영향을 받지 않게 되어 유연한 설계까 가능해진다.
또한, 템플릿 메서드 패턴의 경우 템플릿 클래스를 구현한 구체클래스가 생성될 때 마다 부모클래스의 데이터를 들고있기 때문에 메모리 낭비를 야기한다. 요청 몇십, 몇백건의 경우에는 영향이 없겠지만, 수십만건에 달하는 요청의 경우에는 가비지 컬랙터가 수집해야할 데이터가 많아지고, 가비지 컬렉터가 도는동안은 어플리캐이션이 잠시 멈추기 때문에(Stop-the-world 상태) 성능상 좋지 않다. 전략 패턴을 사용하면 인터페이스를 구체클래스의 메서드만 전달하는 것이 가능하기 때문에 불필요한 데이터를 생성하지 않는다.
여전히 컨트롤러 부분의 코드를 수정해야하는 상황이다. n개의 기능이 있다면 n번의 수정이 발생할 것이기 때문에 절대 유지보수에 좋은 코드가 아니다.
다음에는 컨트롤러에 있는 코드를 수정하지 않고 로깅을 구현하는 방법에 대해 알아보겠다