전략 패턴(Strategy Pattern) ; 알고리즘, 캡슐화, 동적교환

알고리즘군을 정의하고 캡슐화하여 교환해서 사용할 수 있도록 만든다.
전략을 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

객체의 행위를 전략을 통해 동적으로 바꿔 주도록 해서 유연하게 만드는 패턴이다.

책에서 나오는 예시

책에서는 오리의 시뮬레이터를 개발하는 회사에서 개발자가 설계하는 이야기로 전략패턴을 설명하고 있다.
주로 상속과 비교하여 전략패턴을 사용하면 어떤 장점이 있는지 보여준다

만약 flyquack 을 단순히 Duck 클래스에 정의하고 상속을 통해 모든 하위 객체가 구현을 강제화 했다면?,

물론 상속을 통해 중복코드를 간소화 할 수 있었지만, 각 객체마다의 특성이 있는 부분은
(고무오리는 날지 못한다던가, 꽥하고 울지 않는다) 다시 재정의를 해줘야하는 비효율이 발생한다.

따라서 전략패턴은 알고리즘을 별도로 캡슐화하여 정의하고, 이를 교환해서 사용할 수 있도록 하는 패턴이다.

Spring Boot 에서 찾은 전략 패턴

전략 패턴은 Spring 의 수 많은 프로젝트에서 활용된다.

Spring Boot의 구동 시 Banner 라는 인터페이스는 실제 Spring 구동에는 전혀 쓸모없는 녀석이다.
이런점이 조금 Geek(괴짜)한 느낌이 들어서 살펴보게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@FunctionalInterface
public interface Banner {

/**
* Print the banner to the specified print stream.
* @param environment the spring environment
* @param sourceClass the source class for the application
* @param out the output print stream
*/
void printBanner(Environment environment, Class<?> sourceClass, PrintStream out);

/**
* An enumeration of possible values for configuring the Banner.
*/
enum Mode {

/**
* Disable printing of the banner.
*/
OFF,

/**
* Print the banner to System.out.
*/
CONSOLE,

/**
* Print the banner to the log file.
*/
LOG

}

}

역할

Banner 가 맡은 역할은 간단하다. Spring Boot App이 실행될 때, printBanner();를 통해
텍스트로 된 로고를 콘솔이나 로그에 기록하는 역할을 한다.

1
2
3
4
5
6
7
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.7)

그래서 이게 왜 전략패턴?

Banner 인터페이스는 printBanner(); 하나의 메소드만 가지고 있는 인터페이스이다.
따라서, 이를 implements 하는 구현체들은 각각의 알고리즘으로 banner에 띄울 방법을 달리하고 있다.

책에서 봤던 전략 패턴을 그대로 차용하고 있는 것이다.

ImageBanner

커스텀으로 넣은 이미지 파일과 클래스 패스를 포함한 여러 설정 정보를 기반으로 배너로 만들어 준다.

  • 배너에 띄울 이미지 파일
  • 설정 정보(application.properties)
    1
    2
    3
    4
    5
    6
    spring.banner.image.location=classpath:spring.png
    spring.banner.image.pixelmode=block
    spring.banner.image.width=200
    spring.banner.image.invert=true
    #spring.banner.image.height=
    #spring.banner.image.margin=1
  • 배너 실행 결과(이미지를 콘솔이나 로그의 텍스트 기반 배너로 만들어서 띄움)

ImageBanner는 printImage(); 에서
커스텀 이미지 파일을 텍스트로 전환하여 배너로 출력 하는 알고리즘을 수행하게 된다.

ResourceBanner

banner.txt 라는 파일을 작성하고, resources 하위 경로에 두면
banner.txt 안에 작성한 값이 배너로 출력된다.

  • banner.txt
    1
    2
    3
    4
    5
    6
    7
    8
    ,--.  ,--.,------.,--.   ,--.    ,-----.   ,---.,--------.,------.   ,---. ,--------.,------. ,----. ,--.   ,--. ,------.  ,---. ,--------.,--------.,------.,------. ,--.  ,--. 
    | '--' || .---'| | | | ' .-. ' ' .-'--. .--'| .--. ' / O \'--. .--'| .---'' .-./ \ `.' / | .--. '/ O \'--. .--''--. .--'| .---'| .--. '| ,'.| |
    | .--. || `--, | | | | | | | | `. `-. | | | '--'.'| .-. | | | | `--, | | .---.'. / | '--' | .-. | | | | | | `--, | '--'.'| |' ' |
    | | | || `---.| '--.| '--.' '-' ' .-' | | | | |\ \ | | | | | | | `---.' '--' | | | | | --'| | | | | | | | | `---.| |\ \ | | ` |
    `--' `--'`------'`-----'`-----' `-----' `-----' `--' `--' '--'`--' `--' `--' `------' `------' `--' `--' `--' `--' `--' `--' `------'`--' '--'`--' `--'

    ${application.title} ${application.version}
    Powered by Spring Boot ${spring-boot.version}
  • 설정 정보(application.properties) ; Custom 프로퍼티
    1
    2
    application.title=Learn Design Pattern
    application.version=1.0
  • 실행 결과
    1
    2
    3
    4
    5
    6
    7
    8
    ,--.  ,--.,------.,--.   ,--.    ,-----.   ,---.,--------.,------.   ,---. ,--------.,------. ,----. ,--.   ,--. ,------.  ,---. ,--------.,--------.,------.,------. ,--.  ,--. 
    | '--' || .---'| | | | ' .-. ' ' .-'--. .--'| .--. ' / O \'--. .--'| .---'' .-./ \ `.' / | .--. '/ O \'--. .--''--. .--'| .---'| .--. '| ,'.| |
    | .--. || `--, | | | | | | | | `. `-. | | | '--'.'| .-. | | | | `--, | | .---.'. / | '--' | .-. | | | | | | `--, | '--'.'| |' ' |
    | | | || `---.| '--.| '--.' '-' ' .-' | | | | |\ \ | | | | | | | `---.' '--' | | | | | --'| | | | | | | | | `---.| |\ \ | | ` |
    `--' `--'`------'`-----'`-----' `-----' `-----' `--' `--' '--'`--' `--' `--' `------' `------' `--' `--' `--' `--' `--' `--' `------'`--' '--'`--' `--'

    Learn Design Pattern 1.0
    Powered by Spring Boot 2.7.7

SpringBootBanner

ImageBanner, ResourceBanner 없는 경우, 디폴트 배너로서 SpringBootBanner가 동작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class SpringBootBanner implements Banner {

private static final String[] BANNER = { "", " . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\", "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )", " ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/" };

private static final String SPRING_BOOT = " :: Spring Boot :: ";

private static final int STRAP_LINE_SIZE = 42;

@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
for (String line : BANNER) {
printStream.println(line);
}
String version = SpringBootVersion.getVersion();
version = (version != null) ? " (v" + version + ")" : "";
StringBuilder padding = new StringBuilder();
while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) {
padding.append(" ");
}

printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT, AnsiColor.DEFAULT, padding.toString(),
AnsiStyle.FAINT, version));
printStream.println();
}

}

Banners Class

만약 배너로 사용되는 이미지나, banner.txt가 있다면 어떻게 해야하는가?
Spring에선 이런 부분을 Banners라는 클래스를 통해 아주 나이스하게 해결했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static class Banners implements Banner {

private final List<Banner> banners = new ArrayList<>();

void addIfNotNull(Banner banner) {
if (banner != null) {
this.banners.add(banner);
}
}

boolean hasAtLeastOneBanner() {
return !this.banners.isEmpty();
}

@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
for (Banner banner : this.banners) {
banner.printBanner(environment, sourceClass, out);
}
}

}

// Banners 가 각 Banner 구현체를 수집하는 방법
private Banner getBanner(Environment environment) {
Banners banners = new Banners();
banners.addIfNotNull(getImageBanner(environment));
banners.addIfNotNull(getTextBanner(environment));
if (banners.hasAtLeastOneBanner()) {
return banners;
}
if (this.fallbackBanner != null) {
return this.fallbackBanner;
}
return DEFAULT_BANNER;
}

Banner를 구현한 Banners는

  1. ImageBanner, ResourceBanner 가 있다면 해당 banner의 printBanner(); 를 각각 실행
    • 즉, image, resource 배너 둘 다 동시에 사용 혹은 각각 사용 가능
  2. 둘다 없다면 Spring 기본 Banner인 SpringBootBanner의 printBanner(); 를 실행

생각해보자

오픈소스라는 다수의 프로그래머가 하나의 프로덕트에 집중해서 개발한다는 점에 있어서,
‘배너를 어떻게 출력할 것인가?’ 어떻게 보면 굉장히 사소한 문제도 간단히 넘어가는게 없구나 생각이 들었다.
또한, 전략 패턴으로 아주 유연하게 구현해놓은 것을 보고 감탄하면서도 개인적으로 굉장히 재미있다는 생각이 들었다.

Spring 이라는 굉장히 잘 짜여진 프로그램의 소스를 뜯어 본다는 것이 이렇게 즐거운 일일 줄이야..

전략 패턴은 디자인 패턴 중에서도 아주 자주 사용되는 패턴중에 하나이다.
키워드인 알고리즘, 캡슐화, 동작시 교환 이란 측면이 사실상 객체지향의 모든 특징을 거진 다 가지고 있는 패턴이기 때문에
가장 많이 사용되는 디자인 패턴이 아닌가 생각이 든다.

참고자료