스프링과 디자인 패턴 - 전략 패턴

이원석·2024년 2월 20일

Spring

목록 보기
14/20
post-thumbnail
*인프런 김영한 강사님의 강좌를 참고하여 정리한 내용입니다.*

핵심(비즈니스 로직), 보조(로그 추적기, 트랜잭션 등..) 기능의 분리를 위해 템플릿 디자인 패턴을 활용해 보았다.

하지만! 상속으로 인해 부모-자식간의 강한 결합과 응집도가 생기며 이는 유지보수 측면에서 좋지 못한 설계이다.

상속이 아닌 위임으로 문제를 해결하는 전략 패턴에 대해 알아보자!


전략 패턴 (의존성 주입)

1. 구조

전략 패턴은 인터페이스(interface)를 활용한다.

1-1. execute() - Context

변하지 않는 부분(로그 추적, 트랜잭션 기능 등..)의 역활을 하는 메서드이다.

1-2. call() - Strategy

변하는 부분(비즈니스 로직)의 역활을 하는 인터페이스이다.


차이점
1. ContextStrategy 인터페이스에만 의존한다.
2. 핵심 기능 call() 메서드를 인터페이스에 두고 위임을 함으로써 객체간의 결합도를 낮출 수 있다.

다시한번 말하지만, 상속이 아닌 위임으로 문제를 해결한다!



2. 코드

public interface Strategy {
	void call();
}
public class StrategyLogic implements Strategy {
	@Override
    public void call() {
    	log.info("비즈니스 로직 실행!");
    }
}
public class Context {
	private Strategy strategy;
    
    public Context(Strategy strategy) {
    	this.strategy = strategy;
    }
    
    public void execute() {
    	// 공통 로직 살행
        ...
        
        // 비즈니스 로직 실행
        stratagey.call();
        
        ...
    }
}

아닛!!! 어디서 많이 본 것 같은데?


public class OrderController {

	private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

 
    @GetMapping("/request")
    public String request(String itemId) {
    	...
        return "ok";
    }
}

그렇다. 바로 스프링에서 의존관계 주입에 사용하는 방식이 바로 전략 패턴이다!



@Test
void strategyMethod() {
	// 위임
	Strategy strategyLogic = new StrategyLogic();
	Context context1 = new Context(strategyLogic);
    context1.execute();
    
    Context context2 = new Context(new StrategyLogic());
    context2.execute();
}


@Test
void templateMethod() {
	// 상속
	AbstractTemplate template1 = new SubClassLogic1();
    AbstractTemplate template2 = new SubClassLogic2();
    template1.execute();
    tempalte2.execute();
}

  1. Context 생성자에 strategy 구현체 주입
  2. Context 실행 (execute)
  3. execute() 시작 - strategy.call() - execute() 종료

Strategy 인터페이스는 추상메서드를 "하나"가진 함수형 인터페이스이다. 따라서 익명 내부 클래스와 람다를 활용할 수 있다.

@Test
void strategyMethod() {
	// Strategy strategyLogic = new StrategyLogic();
    
    // 익명 내부 클래스
	Context context1 = new Context(new Strategy() {
    	@Override
        public void call() {
        	log.info("비즈니스 로직 실행!");
        }
    });
    
    context1.execute();
    
    // 람다식
    Context context2 = new Context(() -> {
    	log.info("비즈니스 로직 실행!");
    });
    
    context2.execute();
}



3. 선 조립, 후 실행

지금까지의 전략 패턴의 실행 방식을 보면, Context가 실행하기 전에 Strategy를 먼저 필요한 모양으로 조립(의존성 주입) 해두었다. 이러한 방식은 한 번 조립이 끝난 뒤로는 그저 Context를 실행하기만 하면 된다.

마치 스프링에서 애플리케이션 로딩 시점에 의존관계 주입을 마치고(조립), 실제 요청을 처리하는것과 같다.(실행)

이러한 방식에도 단점이 있다고 하는데, 바로 조립을 Strategy를 마치고 나면 전략을 변경하기 번거롭다는 점이다. 만약 Context가 싱글톤이라면 새롭게 Strategy를 setting 하는데 있어서 동시성 이슈가 발생할 수 있다. (사용자A와 사용자B의 요청에 따라 예상치 못한 Strategy가 사용되는 경우)



전략 패턴 (파라미터)

public class Context() {
    
    public void execute(Strategy strategy) {
    	// 공통 로직 살행
        ...
        
        // 비즈니스 로직 실행
        stratagey.call();
        
        ...
    }
}
void strategy() {
	Context context1 = new Context();
    
    // 파라미터 전달
    context.execute(new StrategyLogic());
    
    // 익명 내부 클래스
    context.execute(new Strategy() {
    	@Override
        public void call() {
        	log.info("비즈니스 로직 실행!");
        }
    });
    
    // 람다
    context.execute(() -> {
    	log.info("비즈니스 로직 실행!");
    });
}

선 조립, 후 실행 방식보다 더 유연하게 전략 패턴을 활용하는 방법이 있다.

기존 생성자를 통한 의존성 주입 대신, execute() 메서드의 파라미터로 Strategy를 전달하는것이다.


  1. Context의 execute() 로직을 실행하며 인수로 Strategy를 전달한다.
  2. Context 실행 (execute)
  3. 파라미터로 넘어온 strategy.call() 로직을 실행한다.



의존성 주입 vs 파라미터

두 방식의 차이점은 변경 가능한 핵심 기능 Strategy를 어떻게 전달하느냐에 있다.

의존성 주입

  • Strategy를 필드로 두어 생성자 의존성 주입을 활용함.
  • 선 조립, 후 실행 방식으로 Context의 실행 시점에 이미 조립이 끝났기 때문에 전략에 신경쓸 필요가 없다.
  • 전략을 변경하기에 번거롭다. (유연하지 못함)

파라미터

  • Strategyexecute() 메서드의 인자로 두는 방식.
  • Context의 실행때 마다 전략을 유연하게 변경할 수 있다.
  • 매번 전략을 지정해야 한다.



템플릿 콜백 패턴

스프링에서는 전략 패턴(파라미터) 방식을 템플릿 콜백 패턴이라고 한다. 전략 패턴에서 콜백 부분이 강조된 패턴이다.

Context -> Template
Strategy -> Callback



디자인 패턴(템플릿 메서드, 템플릿 콜백, 전략)을 활용하여 변하는, 변하지 않는 기능을 분리할 수 있었다.

하지만!!

이러한 방식으로는 결국 원본 코드를 수정해야하는 문제가 있다. 구현체에 의존하기 때문이다.

다음을는 원본 코드를 변경하지 않고 사용할 수 있는 디자인 패턴에 대해 알아보자!





참고문헌
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

0개의 댓글