디자인 패턴 (1) - 전략 패턴

2022. 8. 22. 21:20Design Pattern

디자인 패턴이란? 

애플리케이션을 설계하고 구현할 때 특정 맥락에서 자주 발생하는 문제들을 피하기 위해 사용되는 패턴이다.

서비스가 커질수록, 협업 인원이 늘어갈 수록 코드의 양은 많아지며 개발자 마다 코드의 스타일이 달라지기기 쉽다.

이러한 문제가 계속되면 될 수록 버그의 발생 빈도는 높아지고 성능 이슈가 생기게 된다.

디자인 패턴은 의사소통 수단의 일종이자 코드의 효율성을 높이는 수단이다.

디자인 패턴은 알고리즘이나 특정 기술이 아니다 설계방법이자 코딩 방법론일 뿐이기에 

모든 상황에 있어 디자인 패턴을 적용하려 하지 말되 상황마다

적절한 패턴을 이용 하는게 프로그램을 안정적이게 설계 할 수 있다.

 

 

@Slf4j
public class ContextV1Test {
    @Test
    void templateMethod() {
        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);

    }
    
}

위와 같은 로직이 있다고 가정하자

해당 로직을 보면 로직 실행 부분을 제외하면 항상 같은 동작을 한다.

 

이 코드는 매우 비효율 적이다.

 

이를 해결하기 위해 디자인 패턴을 도입 할 수 있다.

 

전략 패턴

출처: 김영한님의 스프링 핵심원리 - 고급편

 

전략 패턴은 변하지 않는 부분과 변하는 부분으로 분리하여 

변하는 부분을 인터페이스로 만들고 해당 인터페이스를 구현하도록 하여 해결한다.

상속이 아니라 위임으로 문제를 해결하는 것.

전략패턴은 알고리즘 제품군을 정리하고 각각 캡슐화하여 상호 교환하게 만드는 것이다.

전략 패턴을 통해 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경 할 수 있다.

 

 

위 코드를 전략 패턴으로 만들기 위해

로직 부분을 인터페이스로 만든다.

 

Startegy 인터페이스

call -> 로직을 call 이라는 메소드에 정의하도록 유도한다.

 

public interface Strategy {
    void call();
}

 

 

위 인터페이스를 상속하여 일반 클래스에서 로직을 구현한다.

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

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

 

주입

1) 필드(생성자) 방식

실제 사용되는 코드에서 Strategy 인터페이스를 생성자로 주입받아 call 메소드를 실행한다.

 

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

    private Strategy strategy;

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

    public void excute() {
        long startTime = System.currentTimeMillis();
        strategy.call(); // 이 부분만 위임
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime => {}", resultTime);
    }
}

 

여기서의 핵심은 Context 클래스는 Strategy 인터페이스에만 의존한다는 점이다. 덕분에 Strategy의 구현체를 변경하거나 새로 만들어도 Context의 코드에는 영향을 받지 않는다.

 

실제로 Spring의 의존관계 주입(DI) 에서도 전략패턴이 사용된다.

@Slf4j
public class ContextV1Test {

    @Test
    void strategyV1() {
        ContextV1 v1 = new ContextV1(new StrategyLogic1());
        v1.excute();
        ContextV1 v2 = new ContextV1(new StrategyLogic2());
        v2.excute();
    }
}

실제로 실행한 결과값이 각각 StrategyLogic1,2에서 따로 구현한 로직이 실행되는것을 볼 수 있다.

간단하게 요약하자면

1. 변경되는 로직을 멤버로 갖고 있는 인터페이스

2. 인터페이스를 구현한 클래스

3. 변경되지 않는 로직을 갖고 있는 클래스 

 

이 세가지를 만든 후

실행 시에 어떤 로직을 수행할건지에 대해 클래스를 선택하여 주입 시키는 것이다. ( 생성자 부분 )

 

@Test
void strategyV2() {
    ContextV1 v1 = new ContextV1(() -> log.info("로직 1 실행"));
    v1.excute();
    ContextV1 v2 = new ContextV1(() -> log.info("로직 2 실행"));
    v2.excute();
}

 

인터페이스이므로 익명 클래스 생성이 가능하고  JAVA 8이상인 경우 메소드를 하나만 갖고 있다면 람다식으로 변형하여 사용도 가능하다.

 

이러한 방식은 선조립, 후 실행 이기 때문에

유연한 사용이 가능하다

스프링에서도 로딩 시점에서 의존관계 주입을 통해 의존관계를 맺고 그 다음에 실제 요청을 처리하는것과 같은 원리이다.

 

허나 이 방식은, 조립 이후 전략을 변경하기가 번거롭다.

Context의 setter를 이용해서 넘겨 받아 할 수 있지만. Context가 싱클톤인 경우 동시성 이슈등 여러 문제가 발생할 수 있다.

 

때문에 필드 (생성자)전략패턴은 전략(내부 로직)이 자주 변경되지 않는 경우에 사용하는 것이 좋다.

 

2) 파라미터 전략패턴

@Slf4j
public class ContextV2 {

    public void excute(Strategy strategy) {
        long startTime = System.currentTimeMillis();
        strategy.call(); // 이 부분만 위임
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime => {}", resultTime);
    }
}

 

단순히 생성자로 안받고 파라미터에 인터페이스를 넣고 해당 파라미터에 구현체를 넣는 것이다.

@Slf4j
public class ContextV2Test {

    @Test
    void strategyV1() {
        ContextV2 context = new ContextV2();
        context.excute(new StrategyLogic1());
        context.excute(new StrategyLogic2());
        context.excute(() -> log.info("로직 3 실행"));
        context.excute(() -> log.info("로직 4 실행"));
    }

}

실행 결과

구현체를 넣을 수도 있고 위와 같이 메소드가 한개인 경우 람다식이 가능하다.

이렇게 된다면  실행시 마다 전략을 유연하게 변경할 수 있게 된다.

좋은것 같지만 반대로 생각해보면 실행 마다 계속해서 전략을 지정해주어야 하는 것이다.

 

이러한 전략 패턴의 핵심

Context 부분은 Strategy 인터페이스에만 의존한다는 점이다.

때문에 Strategy의 구현체의 변경에도 Context 코드에는 영향을 주지 않는다.

 

해당 블로그는 우아한 형제 CTO이신 김영한 님의 강의 스프링 핵심 원리 - 고급편 을 토대로 제작되었습니다.

 

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

 

'Design Pattern' 카테고리의 다른 글

Design pattern  (0) 2023.03.05
디자인패턴 (2) - 프록시 패턴  (0) 2022.08.26