TIL 6 – 템플릿 메서드 패턴

템플릿 메소드 패턴은 생각보다 많이 사용됩니다.

그 중 백엔드에서 Spring을 사용한다면 데이터베이스 작업을 할 때 JdbcTemplate에 대해 들어본 적이 있을 것이다. JdbcTemplate은 실제로 템플릿 메서드 패턴을 사용하여 구현됩니다.

템플릿 메서드 패턴이란 무엇입니까?

GOF에서 연설 템플릿 메서드 패턴의 정의다음과 같다.

알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 전송합니다.
서브클래스가 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계를 재정의할 수 있도록 합니다.

내가 말하는 내용이 이해가 되지 않으면 다음 예제 코드를 살펴보겠습니다.

@Test
void templateMethodV0() {
    logic1();
    logic2();
}

private void logic1() {
    long startTime = System.currentTimeMillis();
    // 비즈니스 로직 실행
    log.info("비즈니스 로직1 실행");
    // 비즈니스 로직 종료
    long endTime = System.currentTimeMillis();
    long resultTime = endTime - startTime;
    log.info("resultTime={}", resultTime);
}

private void logic2() {
    long startTime = System.currentTimeMillis();
    // 비즈니스 로직 실행
    log.info("비즈니스 로직2 실행");
    // 비즈니스 로직 종료
    long endTime = System.currentTimeMillis();
    long resultTime = endTime - startTime;
    log.info("resultTime={}", resultTime);
}

코드의 실행 시간을 표현하는 부분의 코드는 동일하지만 비즈니스 로직을 실행하는 부분은 다릅니다.

객체 지향의 좋은 설계 원칙 중 하나는 다음과 같습니다.

변경되는 부분과 변경되지 않는 부분을 분리하여 캡슐화합니다.

위의 코드를 정의로 보면 변경되지 않는 부분은 코드의 실행 시간을 표현하는 코드 부분입니다.자아, 변경되는 부분은 비즈니스 로직안돼. 이제 위의 Template Method 패턴 정의에 적용해 보자. 알고리즘의 골격은 위와 같이 변하지 않는 부분을 말하는 것입니다.자아, 어떤 단계는 변화하는 부분에 대해 이야기하고 있습니다.안돼.

템플릿 메서드 패턴 구현

템플릿 메소드 패턴을 구현할 때 주로 execute(), call()을 이용하여 구현한다.

위 코드에 템플릿 메소드 패턴을 적용할 수 있으며, 적용하면 다음과 같이 구현할 수 있습니다.

public abstract class AbstractTemplate {

    public void execute() {
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        call(); // 상속
        // 비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }

     protected abstract void call();
}

추상 클래스를 선언함으로써 execute()는 기존 알고리즘 계열의 골격을 작성합니다.

그러나 비즈니스 로직 실행 부분이 다르기 때문에 호출 메서드는 실행될 수 있도록 추상 메서드로 선언합니다.

추상 클래스는 어딘가에 상속되어 구현되어야 하므로 비추상 메서드에서 추상 메서드를 호출하는 것은 문제가 되지 않습니다. 이 부분이 제대로 이해가 되지 않으시면 추상 클래스에 대해서 따로 알아보시면 좋을 것 같습니다.

템플릿 메서드 패턴의 프레임을 만드는 데 추상 클래스가 사용되었으므로 결국 템플릿 메서드 패턴을 사용하려면 추상 클래스를 구현해야 합니까?

public class SubClassLogic1 extends AbstractTemplate {

    @Override
    protected void call() {
        log.info("비즈니스 로직1 실행");
    }
}

public class SubClassLogic2 extends AbstractTemplate {

    @Override
    protected void call() {
        log.info("비즈니스 로직2 실행");
    }
}

이 두 개의 추상 클래스를 상속받은 클래스를 생성하고 추상 메서드를 구현한 것을 확인할 수 있습니다.

템플릿 방법의 장점

Template Method 패턴을 적용하면 위에서 본 샘플 코드가 어떻게 생겼는지 살펴보겠습니다.

    @Test
    void templateMethodV1() {
        AbstractTemplate template1 = new SubClassLogic1();
        template1.execute();

        AbstractTemplate template2 = new SubClassLogic2();
        template2.execute();
    }

위의 긴 코드가 정말 간단하게 단축된 것을 볼 수 있습니다.

같은 패턴인데도 패턴을 바꿔야 한다면 템플릿 방식 패턴이 적용되지 않은 상태에서 해당 패턴을 찾아 코드를 하나씩 바꿔야 했을 것이다. 하지만, 알고리즘의 뼈대를 결정하기 위해 템플릿 메소드 패턴을 적용했으므로 템플릿 메소드의 추상 클래스에서 알고리즘의 뼈대에 해당하는 코드 하나만 수정하면 나머지 부분도 마찬가지다.알 수 있습니다.

어디선가 많이 들어보셨을 것 같은데요? 객체 지향의 좋은 디자인 원칙 SOLID의 하나는 S Single Responsibility Principle입니다.

참고 단일 책임 원칙은 객체가 하나의 책임만 가져야 한다는 원칙입니다.

실제로 객체를 생성하는 과정에서 부품은 하나의 책임만 있는 것 같습니다. 단일 책임 원칙을 따랐는지 여부는 수정할 때 분명합니다. 어떤 서비스 로직을 수정하는 과정에서 여러 객체에 대한 코드를 수정해야 하는 상황이 발생한다면 이는 단일 책임 원칙에 위배되는 것입니다. 사실 이 부분을 추상화해서 바로 적용하기란 쉽지 않습니다. 리팩토링 과정에서 이를 유지할 수 있도록 하는 것이 중요합니다.

템플릿 메소드 패턴을 사용하는 방법을 볼 수 있었습니다. 그것을 보면 다음과 같이 생각할 수 있습니다.

그럼 템플릿 메서드 패턴을 사용하려면 일부 단계에 대해 모든 추상 클래스를 상속받아 구현해야 할까요?

템플릿 메소드 패턴의 다른 부분에서도 같은 코드를 사용해야 한다면 구현하는 것이 좋을 것 같습니다.

그러나 이번에만 실행되고 다시 사용되지 않는다면 다시 구현하는 것은 무리가 될 것입니다.

객체지향의 특징은 재사용과 유지를 용이하게 하기 위해 객체를 생성하고 재사용하는 것이라고 믿습니다. 더미 클래스가 누적됩니다.

따라서 이 문제를 해결하는 방법 중 하나는 익명 클래스 사용방법이 있습니다.

익명 클래스에 대한 설명은 이 게시물에서 찾을 수 있습니다.

이제 익명 클래스가 적용된 테스트 코드를 살펴보겠습니다.

@Test
void templateMethodV2() {
    AbstractTemplate template1 = new AbstractTemplate() {
        @Override
        protected void call() {
            log.info("비즈니스 로직1 실행");
        }
    };
    template1.execute();

    AbstractTemplate template2 = new AbstractTemplate() {
        @Override
        protected void call() {
            log.info("비즈니스 로직2 실행");
        }
    };
    template2.execute();
}

위에서 언급한 것처럼 객체가 생성되지 않아 더미 클래스가 쌓이는 문제가 사라졌다. 그러나 코드의 가독성에 문제가 있습니다. 이것만 봐 “아, 그냥 클래스를 만들어서 사용할게.” 라고 생각할 수 있겠지만 장기적으로 봤을 때 더미 클래스가 100~120개 쌓이면 파일 크기가 커지기 때문에 나름대로 문제가 많을 것이다.

그래서 이 부분은 프로젝트의 규모가 작고 더미 클래스가 많이 생성될 것 같지 않은 경우 가독성을 위해 생성합니다.그리고 만약 프로젝트가 큰 경우 클래스가 꽤 많으므로 더미 클래스를 만들지 않는 것이 좋습니다.

즉, 장단점을 알고 상황에 따라 다른 선택을 하는 것이 중요한 것 같습니다.

템플릿 메서드 패턴의 단점

Template Method 패턴은 항상 좋지만 전부는 아닙니다. 다음과 같은 단점이 있기 때문입니다.

상속받은 클래스 부모 클래스 기능을 추가로 사용하지 않고 무조건 부모 클래스 기능을 사용해야 하는 상황이 있습니다.하다. 결국 템플릿 메서드 추상 클래스를 상속받은 자손 클래스는 부모 클래스의 알고리즘 골격을 수정할 수 없고, 부모 메서드의 기능을 추가로 사용하지 않는 상황이다. 무조건 상속받아야 하는 상황이 되었습니다. 안돼. 결과적으로 하위 클래스가 상위 클래스에 크게 의존하는 상황입니다.좋아요. 강한 의존성은 다음과 같은 결과를 초래할 수 있습니다. 예를 들어 추상 클래스에서 일부 알고리즘 요소를 추가로 구현해야 하는 경우 해당 요소를 다른 추상 메서드로 구현하고 추가하여 execute()에서 실행할 수 있도록 해야 합니다. 이 경우 일부 알고리즘 요소를 구현하는 추상 클래스의 모든 자손을 구현해야 하는 상황에 직면하게 됩니다. 그래서 이렇게 부모에게 크게 의존하는 디자인은 좋은 디자인이 아닙니다.

객체 지향의 좋은 디자인 원칙 중 일부는 다음과 같은 추가 특성을 포함합니다.

상속보다는 복합을 사용하십시오.

템플릿 메소드 패턴의 이러한 상속의 단점을 컴포지션으로 변경하여 이용하는 방법이 있습니다. 전략 패턴안돼.

전략 패턴은 나중에 추가됩니다.