Spring Advanced

Strategy Pattern

cwchoiit 2023. 12. 12. 14:14
728x90
반응형
SMALL
728x90
SMALL

 

템플릿 메서드 패턴에 이어 '전략 패턴(Strategy Pattern)'에 대해 공부한 것을 정리하고자 한다.

 

이 또한 공통 부분을 한 곳에 묶어 중복 코드를 제거하고 변경되는 부분만을 유틸리티성을 부여하여 그때그때 필요한 내용을 채워넣을 수 있게 해주는 패턴이라고 보면 된다.

 

전략 패턴이란 말이 좀 한번에 와닿지 않을 수 있는데 내가 이해한 전략이란건 이 공통 로직을 제외한 변경되는 로직을 처리하는 그 방법을 말한다. 즉, 이 변경되는 로직을 전략이라 말하고 그 전략을 전달받아 실행하는 코드가 있는 것이라고 생각하면 된다.

 

말보다 코드 한 줄이 더 와닿기 때문에 바로 코드로 넘어가보자.

package com.example.advanced.trace.strategy.code.strategy;

public interface Strategy {

    void call();
}

 

'Strategy' 라는 인터페이스를 하나 선언하고 그 인터페이스가 가지는 메서드는 'call()'이다.

그리고 이 인터페이스의 구현체로 두 개의 클래스가 있다.

 

StrategyLogic1.java

package com.example.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class StrategyLogic1 implements Strategy {
    @Override
    public void call() {
        log.info("비즈니스 로직 1 실행");
    }
}

 

StrategyLogic2.java

package com.example.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class StrategyLogic2 implements Strategy {
    @Override
    public void call() {
        log.info("비즈니스 로직 2 실행");
    }
}

 

Strategy 인터페이스를 구현하는 구현체 두 개가 각각 다른 로직의 'call()' 메서드가 있다. 그럼 하나는 전략A, 하나는 전략B가 되는 셈이고 이 전략을 가져다가 사용할 녀석 하나만 만들면 된다. 그리고 그때그때마다 전략을 갈아 끼우는 것 = 전략 패턴이다.

 

이제 실제 전략들을 받아서 사용할 클래스를 하나 만들면 되는데 통상 이 클래스를 가지고 'Context'라고 칭한다. 여기서도 Context라고 표현하겠다.

 

ContextV1.java

package com.example.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

/**
 * 필드에 전략을 보관하는 방식
 * */
@Slf4j
public class ContextV1 {

    private final Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        long startTime = System.currentTimeMillis();

        strategy.call();

        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;

        log.info("resultTime={}", resultTime);
    }
}

 

이 ContextV1 클래스는 전략을 필드로 가지고 있는 방식이다. 즉, 필드로 받기 위해서 전략을 외부에서 주입받게 된다.

그리고 실제 실행 코드는 이 클래스가 가지고 있는 'execute()' 메서드이다.

 

공통 부분과 변경되는 비즈니스 로직 부분을 전략이 가지고 있는 'call()' 메서드를 실행하므로써 채운다.

 

V1 실행

package com.example.advanced.trace.strategy;

import com.example.advanced.trace.strategy.code.strategy.ContextV1;
import com.example.advanced.trace.strategy.code.strategy.Strategy;
import com.example.advanced.trace.strategy.code.strategy.StrategyLogic1;
import com.example.advanced.trace.strategy.code.strategy.StrategyLogic2;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;


@Slf4j
public class ContextV1Test {
    /**
     * 전략 패턴 사용
     * */
    @Test
    void strategyV1() {
        StrategyLogic1 strategyLogic1 = new StrategyLogic1();
        ContextV1 context1 = new ContextV1(strategyLogic1);

        context1.execute();

        StrategyLogic2 strategyLogic2 = new StrategyLogic2();
        ContextV1 context2 = new ContextV1(strategyLogic2);

        context2.execute();
    }

    /**
     * 전략 패턴 익명 내부 클래스 사용
     * */
    @Test
    void strategyV2() {
        ContextV1 context1 = new ContextV1(new Strategy() {

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

        ContextV1 context2 = new ContextV1(new Strategy() {

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

    /**
     * 전략 패턴 익명 내부 클래스를 람다로 사용해서 실행.
     * 람다를 사용하려면 인터페이스에 메서드가 딱 한개만 있어야한다.
     * */
    @Test
    void strategyV3() {
        ContextV1 context1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
        ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));

        context1.execute();
        context2.execute();
    }
}

 

세 가지 다른 방식으로 전략 패턴을 사용한다. 첫번째 'strategyV1()'은 인터페이스의 구현체 객체를 생성하여 Context에 전달한다.

두번째 'strategyV2()'는 익명 내부 클래스 방식으로 구현체의 객체를 생성하는게 아니라 그때마다 구현체를 만들어 낼 수 있다.

세번째 'strategyV3()'는 마찬가지로 익명 내부 클래스 방식이지만 람다 표현식을 사용한다. 

 

이렇게 공통 부분을 한 곳에다 묶고 변경되는 부분을 그때마다 받아 사용하는 방식 중 또 다른 방법인 전략 패턴을 알아봤다.

 

근데, 여기서 끝내면 재미없다. V1이 왜 있겠는가? V2가 있으니 있는거지. V1 방식은 전략을 필드에 보관하는 방식으로 이 방식은 한번 선언된 Context는 영원히 동일한 전략을 가지게 된다. 물론 동일한 전략을 가진 로직을 여러번 사용하는 경우에 이 경우가 더 효율적일 수 있다. 한번만 선언하고 계속 가져다가 사용하면 되니까. 근데 만약 전략이 그때그때 계속 달라져서 달라질 때마다 선언을 하고 초기화를 하고 하는 과정을 거친다면 이는 비효율적이다. 이럴 때 사용할 수 있는 방식이 V2이다. 결론은, V1이 V2보다 좋다 나쁘다가 아니다. 장단점이 있고 상황에 따라 사용될 방식이 달라질 수 있음을 시사한다. 

 

ContextV2.java

package com.example.advanced.trace.strategy.code.strategy;

import lombok.extern.slf4j.Slf4j;

/**
 * 전략을 파라미터로 전달 받는 방식
 * */
@Slf4j
public class ContextV2 {

    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();

        strategy.call();

        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;

        log.info("resultTime={}", resultTime);
    }
}

 

이젠 Context는 필드에 전략을 보관하지 않는다. 대신 실행 코드에서 파라미터로 전략을 전달받는다. 이렇게 되면 그때 그때 전략이 계속 변경될 경우 한번의 초기화만으로 파라미터에 전략만 바꿔 실행하면 된다. 바로 다음 코드처럼.

V2 실행

package com.example.advanced.trace.strategy;

import com.example.advanced.trace.strategy.code.strategy.ContextV2;
import com.example.advanced.trace.strategy.code.strategy.Strategy;
import com.example.advanced.trace.strategy.code.strategy.StrategyLogic1;
import com.example.advanced.trace.strategy.code.strategy.StrategyLogic2;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class ContextV2Test {

    /**
     * 전략 패턴 사용
     * */
    @Test
    void strategyV1() {
        ContextV2 context = new ContextV2();
        context.execute(new StrategyLogic1());
        context.execute(new StrategyLogic2());
    }

    /**
     * 전략 패턴 익명 내부 클래스 사용
     * */
    @Test
    void strategyV2() {
        ContextV2 context = new ContextV2();

        context.execute(new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직 1 실행");
            }
        });

        context.execute(new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직 2 실행");
            }
        });
    }

    /**
     * 전략 패턴 익명 내부 클래스를 람다로 사용
     * */
    @Test
    void strategyV3() {
        ContextV2 context = new ContextV2();

        context.execute(() -> log.info("비즈니스 로직 1 실행"));
        context.execute(() -> log.info("비즈니스 로직 2 실행"));
    }
}

 

Context 객체는 이제 단 한번만 생성하면 된다. 전략의 변경이 있어도 파라미터로 넘겨주면 그만이다. 이러한 장점이 있는 반면, 단점은 같은 전략을 사용한다해도 계속 파라미터로 넘겨줘야 하는 번거로움이 있다. 즉, All-in-One이 아니라 Trade-Off가 있다. 각 상황에 맞게 사용하면 된다.

 

 

728x90
반응형
LIST