고딩왕 코범석

스프링 디자인 패턴(1) 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴 본문

Language & Framework/Spring

스프링 디자인 패턴(1) 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴

고딩왕 코범석 2021. 11. 7. 13:01
반응형

안녕하세요! 이번 포스팅에서는 스프링에서 주로 사용되는 디자인 패턴인 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴에 대해 정리해보려 합니다.

 

가정 상황

이 세 가지 패턴을 설명드리기 앞서 이 포스팅에서는 두 수와 연산(더하기, 빼기 등등..)명령을 입력 받고 수식을 출력한 후 그에 대한 결과를 출력해야하는 상황을 가정하고 디자인 패턴을 설명드리겠습니다.

 

템플릿 메서드 패턴

정의

동작 상의 알고리즘의 뼈대를 정의하고, 알고리즘의 구조를 변경하지 않고 변하는 부분을 다시 정의할 수 있는 디자인 패턴

 

우선, 해당 패턴에서 두 수와 연산 명령을 입력받고 연산식과 결과를 출력하는 함수를 추상 클래스로 두겠습니다.

 

@Slf4j
public abstract class AbstractCalculate {

    public void calc(int a, int b, String cmd) {
        log.info("{} {} {}", a, cmd, b);
        int answer = calculate(a, cmd, b);
        log.info("결과는 {}입니다.", answer);
    }

    protected abstract int calculate(int a, String cmd, int b);
}

 

이번에는 더하기 연산을 처리하는 PlusCalculate를 구현해보겠습니다.

 

public class PlusCalculate extends AbstractCalculate{

    @Override
    protected int calculate(int a, String cmd, int b) {
        if (cmd.equals("+")) {
            return a + b;
        }
        throw new IllegalArgumentException("더하기가 아닙니다!");
    }
}

 

그 다음은 빼기 연산을 처리하는 MinusCalculate를 구현해보겠습니다.

 

public class MinusCalculate extends AbstractCalculate{
    @Override
    protected int calculate(int a, String cmd, int b) {
        if (cmd.equals("-")) {
            return a - b;
        }
        throw new IllegalArgumentException("빼기가 아닙니다!");
    }
}

 

이제 더하기를 호출하는 클라이언트 클래스를 보겠습니다.

 

public class Client {

    public void main() {
        AbstractCalculate plus = new PlusCalculate();
        plus.calc(1, 2, "+");

        AbstractCalculate minus = new MinusCalculate();
        minus.calc(1, 2, "-");
    }
}

 

이제 실행시켜보면

image

예상대로 결과가 잘 나온다는 것을 알 수 있습니다.

 

단점

템플릿 메소드 패턴의 특징은 구현 클래스가 추상 클래스와 강하게 결합되어있습니다. 저는 처음에 개발 서적에서 나오는 강하게 결합 되어있다 라는 표현이 이해가 가지 않았는데, 이번 디자인 패턴을 공부하면서 어떤 의미인지 체감할 수 있게 되었습니다.

 

해당 패턴에서 강하게 결합되어있다는 말은 즉, 부모 클래스가 변경될 때, 자식 클래스도 바로 영향을 받는다는 것입니다.

예를 들어, 추상 클래스의 결과 반환 타입이 int로 되어있지만 클라이언트가 소수점으로 출력받길 원할 경우는 AbstractCalculate의 calculate 메서드의 반환 타입을 float나 double 타입으로 바꿔야 합니다. 이렇게 되면 앞서 구현했던 PlusCalculate, MinusCalculate의 메서드도 변경됩니다.

 

전략 패턴

정의

실행 중에 알고리즘을 선택할 수 있게 하는 디자인 패턴

 

이번에는 Context라는 클래스를 생성해 앞의 템플릿 메서드 패턴에서 추상 클래스에 넣어두었던 연산식 출력과 결과 출력 기능을 넣겠습니다.

 

@Slf4j
public class Context {

    private final CalculateStrategy strategy;

    public void execute(int a, String cmd, int b) {
        log.info("{} {} {}", a, cmd, b);
        int answer = strategy.calculate(a, cmd, b);
        log.info("결과는 {}입니다.", answer);
    }
}

 

그리고 CalculateStrategy 입니다.

 

public interface CalculateStrategy {

    int calculate(int a, String cmd, int b);
}

 

CalculateStrategy를 구현한 Plus, Minus Calculate 클래스들 입니다.

 

public class PlusStrategy implements CalculateStrategy{

    @Override
    public int calculate(int a, String cmd, int b) {
        if (cmd.equals("+")) {
            return a + b;
        }
        throw new IllegalArgumentException("더하기가 아닙니다!");
    }
}

 

public class MinusStrategy implements CalculateStrategy{
    @Override
    public int calculate(int a, String cmd, int b) {
        if (cmd.equals("-")) {
            return a - b;
        }
        throw new IllegalArgumentException("빼기가 아닙니다!");
    }
}

 

그리고 클라이언트 코드입니다.

public class Client {

    public void main() {
        Context plusContext = new Context(new PlusStrategy());
        plusContext.execute(1, "+", 2);

        Context minusContext = new Context(new MinusStrategy());
        minusContext.execute(1, "-", 2);
    }
}

 

image

결과도 잘 출력되는 것을 알 수 있습니다.

 

템플릿 메서드 패턴과 비교하자면, Abstract 클래스에 변경이 있을 경우 변경 여파가 구현 클래스들까지 퍼진다는 단점이 있었습니다. 하지만 전략 패턴은 Context 클래스의 수정에 대한 영향이 없다는 점 입니다. Context 클래스가 구현체가 아닌 추상(인터페이스)에 의존했기 때문에 변경 여파가 최소화 됩니다.

단점

하지만 전략이 달라질 때 마다 이 Context 클래스의 객체를 생성해줘야 한다는 단점이 있었습니다. 클라이언트 코드를 보면 더하기 빼기 전략을 선택할 때 마다 Context 클래스를 생성해줬는데요. 사실상 Context 객체는 공통 로직임에도 계속 호출하여 객체를 생성하게 된다면 메모리 낭비입니다.

 

파라미터 방식으로 개선

이번에는 파라미터 방식으로 개선해볼게요. 기존의 Context 클래스를 다음과 같이 수정해보겠습니다.

 

@Slf4j
public class Context {
    public void execute(int a, String cmd, int b, CalculateStrategy strategy) {
        log.info("{} {} {}", a, cmd, b);
        int answer = strategy.calculate(a, cmd, b);
        log.info("결과는 {}입니다.", answer);
    }
}

 

그에 따라 변경된 클라이언트 코드입니다.

 

public class Client {

    public void main() {
        Context context = new Context();
        context.execute(1, "+", 2, new PlusStrategy());
        context.execute(1, "-", 2, new MinusStrategy());
    }
}

 

이전에 Context 클래스의 필드에 인터페이스를 둔것과 달리, 전략에 따라 파라미터에 인터페이스를 넣기 때문에 Context 클래스 객체를 하나만 생성해도 전략에 대한 구현체만 바꿔 호출합니다.

 

템플릿 콜백 패턴

템플릿 콜백 패턴은 GoF 디자인 패턴은 아니지만, 스프링에서 굉장히 많이 쓰이는 패턴입니다. 스프링의 XXXTemplate 클래스들이 대표적인 예시입니다.

 

Callback

위키에 나온 콜백의 정의는 다음과 같습니다.

 

다른 코드의 인수로 넘겨주는 실행 가능한 코드를 의미하며, 필요에 따라 즉시 실행 혹은 나중에 실행할 수 있다.

 

또한 콜백 코드를 전달할 때는 콜백 함수의 포인터, 서브루틴 또는 람다함수의 형태로 넘겨준다

 

저는 이 콜백이라는 단어에 대해 생소했습니다. 주로 자바스크립트 관련글을 참고했을 때 접했던 단어인데 자바에서는 이 콜백이 어떤 의미로 다가가야 하나 고민을 했었는데 위키의 람다함수를 보고 바로 이해했습니다.

 

이제 템플릿 콜백 패턴으로 적용해보겠습니다. 방법은 파라미터에 인터페이스 구현체를 넣어주는 것이 아닌, 익명 함수를 작성하는 것입니다.

 

public class Client {

    public void main() {
        Context context = new Context();
        context.execute(1, "+", 2, (a, cmd, b) -> {
            if (cmd.equals("+")) {
                return a + b;
            }
            throw new IllegalArgumentException("더하기가 아닙니다!");
        });
        context.execute(1, "-", 2, (a, cmd, b) -> {
            if (cmd.equals("-")) {
                return a - b;
            }
            throw new IllegalArgumentException("빼기가 아닙니다!");
        });
    }
}

 

위와 같이 별도의 구현체 클래스를 작성하지 않는 방법이 있습니다.

위 세가지 패턴의 공통점?

변하지 않는 부분과 자주 변할 수 있는 부분을 분리했다 는 점입니다. 입력값을 받고 출력해주는 기능들을 하나의 공통적인 관심사로 두어 한 곳에서 관리하게끔 추상 클래스나 클래스의 한 곳에 직접 구현을 했고, 자주 변하는 부분은 추상 클래스를 상속하거나 인터페이스로 두어 구현했습니다.

 

이렇게 되면 만약 변하지 않는 부분(포스팅의 예제에서는 입력값을 받고 연산식과 결과를 출력하는 기능)이 어쩔 수 없이 변할 경우가 생겼을 때, 이 변하지 않는 코드들을 공통적으로 추출하지 않고 여기저기 흩어져있을 때 유지보수가 상당히 힘들어 질 것입니다.

물론 위의 세 가지 패턴을 사용하더라도 변하지 않는 부분이 변하게 되면 수정이 필요합니다. 하지만 컴파일 에러로 IDE가 체크를 해주기 때문에 변경 사항에 대해 놓치지 않을 수 있다는 장점이 있습니다.

 

이번 포스팅은 여기서 마무리하겠습니다. 언제나 틀린 부분에 대한 피드백이나 의견은 항상 환영합니다!

출처

 

스프링 핵심 원리 - 고급편

 

https://ko.wikipedia.org/wiki/%ED%85%9C%ED%94%8C%EB%A6%BF_%EB%A9%94%EC%86%8C%EB%93%9C_%ED%8C%A8%ED%84%B4

 

https://ko.wikipedia.org/wiki/%EC%A0%84%EB%9E%B5_%ED%8C%A8%ED%84%B4

 

https://ko.wikipedia.org/wiki/%EC%BD%9C%EB%B0%B1

반응형