2. 부가 기능 추가 (다른 로직을 추가하는 등의부가 기능을 수행, 예) 실행 시간을 측정해서 로그를 남긴다.)
* GOF 디자인 패턴에서는 의도에 따라서프록시 패턴과, 데코레이터 패턴으로 구분한다.
- 프록시 패턴 :접근 제어가 목적
- 데코레이터 패턴 :새로운 기능 추가가 목적
프록시 패턴의 장점
- 부가 기능을 추가하여서 원하는 흐름에 맞게 조정할 수 있습니다.
코드
* 메시지를 출력하는 기능에 메시지를 꾸며주는 기능을 데코레이터 패턴을 이용해서 적용해 보겠습니다.
Component.java
- interface로 Component 생성
public interface Component {
String operation();
}
RealComponent.java
- 실제 Component 구현체
@Slf4j
public class RealComponent implements Component{
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
DecoratorPatternClient.java
- Component를 실행하는 클라이언트 클래스
@Slf4j
public class DecoratorPatternClient {
private Component component;
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute() {
String result = component.operation();
log.info("result={}", result);
}
}
DecoratorPatternTest.java
- 기본 메시지 출력 기능 테스트
@Slf4j
public class DecoratorPatternTest {
@Test
void noDecorator() {
Component realComponent = new RealComponent();
DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
client.execute();
}
}
결과 : RealComponent 실행
result=data
MessageDecorator.java
- 데코레이터 패턴을 이용해서 메시지 꾸며주는 기능 추가
- MessageDecorator는 Component 인터페이스를 구현한다.
@Slf4j
public class MessageDecorator implements Component{
private Component component;
public MessageDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("MessageDecorator 실행");
String result = component.operation();
String decoResult = "<< =====" + result + " ===== >>";
log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후 ={}",result, decoResult);
return decoResult;
}
}
DecoratorPatterTest.java
- 클라이언트가 호출하기 전에 MessageDecorator 클래스를 이용해서 부가 기능을 추가 후 클라이언트에게 messageDecorator를 넘겨준다. messageDecorator도 Component를 구현하고 있기 때문에 클라이언트 입장에서는 전혀 문제가 발생하지 않는다.
@Slf4j
public class DecoratorPatternTest {
@Test
void decorator1() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
client.execute();
}
}
결과 :
정리 :
데코레이터 패턴의 의도 : 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공
탬플릿 메서드 패턴은 부모 클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두어서 상속을 사용해서 문제를 해결하였습니다. 전략 패턴은 변하지 않는 부분을 Context 라는 곳에 두고, 변하는 부분을 Strategy 라는 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결합니다.
상속이 아니라 위임으로 문제를 해결하는 것이다.
전략 패턴에서 Context 는 변하지 않는 템플릿 역할을 하고, Strategy 는 변하는 알고리즘 역할을 합니다.
전략 패턴 의도
알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자.
전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있습니다.
코드
방식1. 필드에 전략을 보관하는 방식
ContextV1.java
@Slf4j
public class ContextV1 {
private 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);
}
}
변하는 알고리즘
StrategyLogin1.java
@Slf4j
public class StrategyLogin1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
StrategyLogin2.java
@Slf4j
public class StrategyLogin2 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
}
테스트 코드
ContextV1Test.java
@Slf4j
public class ContextV1Test {
@Test
void strategyV1() {
StrategyLogin1 strategyLogin1 = new StrategyLogin1();
ContextV1 contextV1 = new ContextV1(strategyLogin1);
contextV1.execute();
StrategyLogin2 strategyLogin2 = new StrategyLogin2();
ContextV1 contextV2 = new ContextV1(strategyLogin2);
contextV2.execute();
}
}
전략 패턴 사용은 선 조립, 후 실행으로 생각하면 보다 간단합니다.
변하지 않는 코드 ContextV1 객체에 변하는 알고리즘인 StrategyLogin1, StrategyLogin2를 생성자에 넣어서 조립하고, 호출이 필요한 시점에 execute()를 이용해서 실행합니다. 스프링 애플리케이션 개발 할 때 애플리케이션 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고 다음에 실제 요청을 처리하는 것과 같은 원리이다.
위와 같이 사용하게 되면 변하는 알고리즘인 전략 class가(StrategyLogin1,StrategyLogin2) 계속 생겨날 수밖에 없습니다.
이러한 점을 보완하기 위해서 익명 내부 클래스를 사용합니다.
익명 내부 클래스 사용
@Slf4j
public class ContextV1Test {
@Test
void strategyV2() {
Strategy strategyLogic1 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
};
ContextV1 contextV1 = new ContextV1(strategyLogic1);
contextV1.execute();
Strategy strategyLogic2 = new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
};
ContextV1 contextV2 = new ContextV1(strategyLogic2);
contextV2.execute();
}
}
람다를 이용해서 보다 더 코드 수를 줄일 수 있습니다.
@Slf4j
public class ContextV1Test {
@Test
void strategyV3() {
ContextV1 contextV1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
contextV1.execute();
ContextV1 contextV2 = new ContextV1(() -> log.info("비즈니스 로직2 실행"));
contextV2.execute();
}
}
방식2. 필드에 전략을 보관하는 방식
전략을 파라미터로 전달 받는 방식, 실행할 때 마다 전략을 유연하게 변경할 수 있다. 단점 역시 실행할 때 마다 전략을 계속 지정해주어야 한다.
ContextV2.java
@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);
}
}
테스트 코드
ContextV2Test.java
@Slf4j
public class ContextV2Test {
/**
* 전략 패턴 적용
*/
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogin1());
context.execute(new StrategyLogin2());
}
/**
* 전략 패턴 익명 내부 클래스
*/
@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 실행");
}
});
}
/**
* 전략 패턴 익명 내부 클래스2, 람다
*/
@Test
void strategyV3() {
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직 1 실행"));
context.execute(() -> log.info("비즈니스 로직 2 실행"));
}
}
참고로 ContextV2 형식의 전략 패턴은 스프링 내부에서 템플릿 콜백 패턴이라고 부른다.
스프링에서 jdbcTemplate, RestTemplate, ~Template 처럼 다양한 템플릿 콜백 패턴이 사용된다.