본문 바로가기

Design Pattern

[Design Pattern] 전략 패턴(Strategy Pattern) - (1)

[Design Pattern] - [Design Pattern] 전략 패턴(Strategy Pattern) - (2)

 

대부분 프로젝트를 시작하기 전에 큰 틀을 만들어놓고 시작하지만 한정된 프로젝트 기간과 개발을 진행하는 개발자들에 의해서 너무나도 다른 소스 코드가 작성된다.

3년 동안 5번의 프로젝트를 경험하면서 나를 비롯한 대부분의 개발자들의 소스는 if와 for문으로 주저리주저리 나열되는 소스 코드를 작성하고 있었다는 것을 알 수 있었다.

자바라는 언어 자체가 이렇게 소스 코딩을 하라고 만들어진 언어가 아닐 텐데...

이러한 소스 코드는 하나의 문자열을 수정하려고 해도 또다시 if 문을 사용해야 하는 일이 발생하게 되거나 중복된 기능을 여러 곳에 의미 없이 복사+붙여 넣기를 해야 하는 등 비효율적인 일의 발생으로 인해 개발에 대한 회의감과 또 야근을 해야 한다는 절망감에 빠지게 만든다.

 

'그렇다면 어떻게 해야 할까?'라는 고민을 굉장히 많이 해왔던 것 같다.

답은 생각보다 간단했다. 포기하거나 나아가는 것이다.

포기란 개발자를 그만두거나 현실에 그냥 순응하면서 사는 것을 말하며, 나아가는 것은 더 나은 개발자가 되기 위해서 내가 할 수 있는 모든 것을 해보는 것이다.

우리는 갓 물주나 통장에 100억씩 찍혀있는 재벌 2, 3세가 아니지 않은가?

이러나저러나 어차피 밑바닥부터 시작해서 고생이란 고생은 다해야 할 인생, 누가 이기나 하는 심정으로 끝까지 도전을 해보는 것이 가장 좋은 선택지인 것 같다.

 

사실 디자인 패턴 중 전략 패턴(Strategy Pattern)을 공부하면서 너무 어려워서 무슨 말이라도 해보고 싶었다. 어차피 누군가에게 보여주기 위한 포스팅이 아니기 때문에 앞으로 답답하면 이렇게 일기라도 써야겠다.

전략 패턴 개념 자체가 어려운 것은 아닌데, 이 패턴을 실제로 적용하기 위해서 생각하고 이해를 하는 것 자체가 너무 어렵다. 역시 나는 A1 다음은 A2나 생각할 줄 아는 단순한 놈인가 보다.

 

사설은 그만하고 이제 집중을 해보자...

 

 

 

전략 패턴(Strategy Pattern) 또는 정책 패턴(Policy Pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.

 

객체들이 할 수 있는 행위 각각에 대하여 전략 클래스를 생성하고, 유사한 행위들을 캡슐화하는 인터페이스를 정의하여 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말한다.

간단히 말해서 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 하는 패턴이다.

전략 패턴(Strategy Pattern)을 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

 

 

전략 패턴(Strategy Pattern)을 구성하는 3가지 요소

  1. 전략 메서드를 가진 전략 객체
  2. 전략 객체를 사용하는 컨텍스트(전략 객체의 사용자, 어떤 객체를 핸들링하기 위한 접근 수단)
  3. 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(전략 객체의 공급자)

 

https://t1.daumcdn.net/cfile/tistory/99C77B475ACBFE171F

 

클라이언트는 다양한 전략 중 현 상황에 적합한 전략을 생성하여 컨텍스트에 주입한다.

 

 

https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F264BF550575F910011

 

Strategy pattern diagram

 

 


 

 

예제를 통해서 전략 패턴(Strategy Pattern)에 대해서 더 자세히 이해를 해보도록 하자.

차의 종류는 승용차와 SUV가 있고 이 차들은 각각 휘발유와 경유를 주유한다고 가정해보자.

 

 

 

1. Strategy Interface(전략 인터페이스)

  • 승용차와 SUV의 "주유"는 공통된 행위로 "전략"이 되며 "캡슐화" 시킬 수 있게 된다.
1
2
3
4
5
package com.acma.pattern.nonocp.service;
 
public interface 주유 {
    void 주유하다();
}
Colored by Color Scripter
 

 

 

 

2. Concrete Class(구체 클래스, 구현 클래스)

  • 이러한 객체의 행위를 동적으로 바꾸기 위해선 "전략 인터페이스"를 직접 수정하지 않고 전략 인터페이스를 주입받는 구체 클래스에서 구현하는 방법으로 전략을 확장할 수 있다.

◈ 승용차 구체 클래스(승용차 Concrete Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.acma.pattern.nonocp.service.impl;
 
import com.acma.pattern.nonocp.service.주유;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class 승용차 implements 주유 {
 
    @Override
    public void 주유하다() {
        log.info("휘발유를 넣는다.");
    }
}
Colored by Color Scripter
 

 

◈ SUV 구체 클래스(SUV Concrete Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.acma.pattern.nonocp.service.impl;
 
import com.acma.pattern.nonocp.service.주유;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class SUV implements 주유 {
 
    @Override
    public void 주유하다() {
        log.info("경유를 넣는다.");
    }
}
Colored by Color Scripter
 

 

 

 

3. 컨텍스트(Context)

  • 객체를 핸들링하기 위한 접근 수단으로 생성된 전략 객체를 주입받는다.
  • 주유소 생성자의 파라미터인 "주유 캡슐화"는 클라이언트에 의해서 생성된 "전략 객체"이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.acma.pattern.nonocp.service.impl;
 
import com.acma.pattern.nonocp.service.주유;
 
public class 주유소 {
    private 주유 주유캡슐화;
    
    public 주유소(주유 주유캡슐화) {
        this.주유캡슐화 = 주유캡슐화;
    }
    
    public void 주유소에서주유하기() {
        주유캡슐화.주유하다();
    }
}
Colored by Color Scripter
 

 

 

 

4. 클라이언트(Client) - 실행

  • 생성된 전략 객체의 공급자전략 객체를 생성하여 컨텍스트에 주입한다.
  • 생성한 전략 객체(승용차 또는 SUV)를 컨텍스트(Context)에 주입 후 Context에 선언된 메서드를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.acma.pattern.nonocp;
 
import org.junit.Test;
 
import com.acma.pattern.nonocp.service.주유;
import com.acma.pattern.nonocp.service.impl.SUV;
import com.acma.pattern.nonocp.service.impl.승용차;
import com.acma.pattern.nonocp.service.impl.주유소;
 
public class 전략패턴 {
 
    @Test
    public void 전략패턴테스트() {
        
        주유소 주유하기 = null;
        주유 승용차 = new 승용차();
        주유하기 = new 주유소(승용차);
        주유하기.주유소에서주유하기();
        
        주유 suv = new SUV();
        주유하기 = new 주유소(suv);
        주유하기.주유소에서주유하기();
    }
}
Colored by Color Scripter
 

 

 

 

5. 결과

클라이언트를 실행한 결과이다.

 

 

소스 코드를 자세히 보면 알겠지만 템플릿 메서드 패턴과의 차이점은 "추상 클래스를 상속"받는 것인지 "인터페이스를 implements"하는 것인지 정도이지만 단일 상속만이 가능한 자바에서 상속이라는 제한이 있는 템플릿 메서드 패턴보다는 다양하게 많은 전략을 implements 할 수 있는 전략 패턴을 많이 사용할 것으로 보인다.

 

 

 

위의 소스 코드 자체는 문제가 없어 보인다. 하지만 사실문제가 발생할 수 있도록 소스 코드의 예제를 만들었다.

객체 지향 설계의 5원칙과 함께 위의 예제를 수정해보도록 하자.

 

위에서 차의 종류는 승용차와 SUV가 있고 이 차들은 각각 휘발유와 경유를 주유한다고 가정하였다.

위의 가정에 더해서 경유를 주유하는 승용차와 휘발유를 주유하는 승용차가 개발되었다고 가정해보자.

 

그러면 우리는 승용차의 주유하다() 메서드와 SUV의 주유하다() 메서드를 아래와 같이 수정해야 할 것이다.

 

◈ 승용차 구체 클래스(승용차 Concrete Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.acma.pattern.nonocp.service.impl;
 
import com.acma.pattern.nonocp.service.주유;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class 승용차 implements 주유 {
 
    @Override
    public void 주유하다() {
        log.info("경유를 넣는다.");
    }
}
Colored by Color Scripter
 

◈ SUV 구체 클래스(SUV Concrete Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.acma.pattern.nonocp.service.impl;
 
import com.acma.pattern.nonocp.service.주유;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class SUV implements 주유 {
 
    @Override
    public void 주유하다() {
        log.info("휘발유를 넣는다.");
    }
}
Colored by Color Scripter
 

하지만 이렇게 수정하는 것은 객체 지향 설계 5원칙, SOLID의 원칙 중 객체는 확장에 대해서 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 "개방-폐쇄 원칙(OCP, Open-Closed Principle)"에 위배가 된다.

OCP에 의하면 기존의 주유하다() 메서드를 수정하지 않으면서 행위가 수정되어야 하지만 위의 소스 코드는 승용차, SUV의 주유하다() 메서드를 직접 수정하였기 때문이다.

 

위의 소스 코드와 같은 방식의 변경은 시스템이 확장되었을 경우 유지보수를 어렵게 하는 방식이다.

예를 들어 승용차, SUV 등의 차의 유종으로 천연가스, LPG, 수소에너지, 전기에너지 등의 새로운 주유 자원이 생기거나 오토바이, 트럭, 버스 등의 새로운 차 종이 생길 경우 주유하다 메서드를 여러 클래스에서 하나하나 정의하여야 하며 이로 인해 메서드의 중복이 발생할 것이기 때문이다.

 

위의 소스 코드와 같이 잘못된 전략 패턴의 문제점은 다음과 같다.

  • 개방-폐쇄 원칙(OCP, Open-Closed Principle) 위배
  • 시스템의 확장 시 메서드의 중복 발생

 


전략 패턴 코딩 다시 하기!

 

 

1. 전략 생성

위의 방법은 승용차 전략 클래스와 SUV 전략 클래스를 기준으로 이 객체들의 공통된 행위인 "주유하다"로 "주유"라는 인터페이스를 생성한 경우이다.

 

조금 더 신중하게 생각해보면 "주유"라는 인터페이스는 승용차 전략 클래스와 SUV 전략 클래스를 기준으로 하는 것이 아닌 유종을 기준으로 생성되어야 하며, 이 인터페이스를 implements(구현)하는 클래스는 휘발유 전략 클래스와 경유 전략 클래스가 되어야 한다는 것을 알 수 있을 것이다. 이처럼 차종이 아닌 유종을 기준으로 전략 패턴을 생성해야 경유를 주유하는 승용차가 출시되거나 휘발유를 주유하는 SUV가 출시되었을 때, OCP를 위배하지 않을 수 있고 또한 새로운 차종을 추가할 경우 메서드의 중복 사용을 막을 수 있다.

 

위의 소스 코드를 아래와 같이 변경하면서 이해하도록 하자.

 

현재 주유 방법(방법이라고 하기에는 문제점이 있어 보이지만 편의상 자원이 아닌 방법이라고 하도록 한다)은 휘발유 넣기와 경유 넣기 두 가지 방식(유종)이 있다.

 

주유하는 방식(유종)에 대해 Strategy Class를 생성하도록 한다.

  • 휘발유전략 클래스
  • 경유전략 클래스

그리고 두 클래스는 주유하다() 메서드를 구현하여 어떤 유종을 주유하는지에 대해 구현한다.

 

또한 두 전략 클래스를 캡슐화하기 위해서 주유전략 인터페이스를 생성한다.

이렇게 캡슐화를 하는 이유는 주유 방법(유종)에 대한 전략뿐만 아니라 다른 전략들(예를 들어 운송수단이 달리는 길 등)이 추가적으로 확장되는 경우를 고려해야 하기 때문이다.

 

 

1) 주유전략 인터페이스

1
2
3
4
5
package com.acma.pattern.strategy.service;
 
public interface 주유전략 {
    public void 주유하다();
}
Colored by Color Scripter
 

2) 휘발유전략 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.acma.pattern.strategy.service.impl;
 
import com.acma.pattern.strategy.service.주유전략;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class 휘발유전략 implements 주유전략 {
 
    @Override
    public void 주유하다() {
        log.info("휘발유를 주유한다.");
    }
}
Colored by Color Scripter
 

3) 경유전략 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.acma.pattern.strategy.service.impl;
 
import com.acma.pattern.strategy.service.주유전략;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class 경유전략 implements 주유전략 {
 
    @Override
    public void 주유하다() {
        log.info("경유를 주유한다.");
    }
}
Colored by Color Scripter
 

 

 

 

 

2. 주유 방법에 대한 클래스 정의

승용차와 SUV 같은 자동차는 주유하다() 메서드를 통해 자동차의 연료를 주입할 수 있습니다.

이때 주유 방식을 직접 메서드로 구현하지 않고 어떻게 주유할 것인지에 대한 전략을 설정하여 그 전략의 주유 방식을 사용하여 주유하도록 하기 위하여 전략을 설정하는 메서드인 set주유전략() 메서드를 선언한다.

 

 

1) 주유 클래스(컨텍스트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.acma.pattern.strategy;
 
import com.acma.pattern.strategy.service.주유전략;
 
public class 주유 {
    private 주유전략 주유전략캡슐;
    
    public void 주유하다() {
        주유전략캡슐.주유하다();
    }
    
    public void set주유전략(주유전략 주유전략캡슐) {
        this.주유전략캡슐 = 주유전략캡슐;
    }
}
Colored by Color Scripter
 

 

2) 승용차 클래스

1
2
3
4
5
package com.acma.pattern.strategy;
 
public class 승용차 extends 주유 {
 
}
Colored by Color Scripter
 

 

3) SUV 클래스

1
2
3
4
5
package com.acma.pattern.strategy;
 
public class SUV extends 주유 {
 
}
Colored by Color Scripter
 

 

 

 

 

3. Client 구현

승용차와 SUV 객체를 생성한 후 각 자동차가 어떤 방식으로 주유를 하는지 설정하기 위해 set주유전략() 메서드를 호출한다.

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
package com.acma.pattern.strategy;
 
import org.junit.Test;
 
import com.acma.pattern.strategy.service.impl.경유전략;
import com.acma.pattern.strategy.service.impl.휘발유전략;
 
public class 전략패턴 {
    
    @Test
    public void 전략패턴테스트() {
        주유 승용차 = new 승용차();
        승용차.set주유전략(new 휘발유전략());
        승용차.주유하다();
        
        승용차.set주유전략(new 경유전략());
        승용차.주유하다();
        
        주유 SUV = new SUV();
        SUV.set주유전략(new 경유전략());
        SUV.주유하다();
        
        SUV.set주유전략(new 휘발유전략());
        SUV.주유하다();
    }
}
Colored by Color Scripter
 

 

 

 

 

4. 결과

클라이언트를 실행한 결과이다.

경유를 주유하는 승용차, 휘발유를 주유하는 SUV가 개발되었다는 상황을 만들어 자동차의 주유 전략을 생성한 결과 전략 패턴을 사용하면 프로그램 상으로 로직이 변경되었을 때, 얼마나 유연하게 수정을 할 수 있는지 알 수 있다.

 

 

 

그런데 말입니다.

주유를 "상속"하면 승용차의 종류, 달리는 도로의 종류 등의 행위를 추가하는 것에 문제가 있어 보인다.

정말이지 "디자인 패턴"의 길은 멀고도 험난한 길인 것 같다.

 

 


 

전략 패턴 코딩 또다시 하기!

  • 전략 패턴에 대해서 제가 잘못 이해하고 있을 수 있습니다. 언제든지 잘못된 점에 대해서는 알려주세요. 잘못된 점에 대한 지적은 칭찬보다 더 값진 관심입니다.

 

1) 주유전략 인터페이스

1
2
3
4
5
package com.acma.pattern.strategynonextends.service;
 
public interface 주유전략 {
    public void 주유하다();
}
Colored by Color Scripter
 

 

2) 휘발유전략 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.acma.pattern.strategynonextends.service.impl;
 
 
import com.acma.pattern.strategynonextends.service.주유전략;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class 휘발유전략 implements 주유전략 {
 
    @Override
    public void 주유하다() {
        log.info("휘발유를 주유한다.");
    }
}
Colored by Color Scripter
 

 

3) 경유전략 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.acma.pattern.strategynonextends.service.impl;
 
 
import com.acma.pattern.strategynonextends.service.주유전략;
 
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public class 경유전략 implements 주유전략 {
    
    @Override
    public void 주유하다() {
        log.info("경유를 주유한다.");
    }
}
Colored by Color Scripter
 

 

4) 자동차컨텍스트 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.acma.pattern.strategynonextends.service;
 
public class 자동차컨텍스트 {
    private 주유전략 주유전략캡슐;
    
    public void 주유하다() {
        주유전략캡슐.주유하다();
    }
 
    public void set주유전략(주유전략 주유전략캡슐) {
        this.주유전략캡슐 = 주유전략캡슐;
    }
}
Colored by Color Scripter
 

 

5) Client

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
package com.acma.pattern.strategynonextends;
 
import org.junit.Test;
 
import com.acma.pattern.strategynonextends.service.자동차컨텍스트;
import com.acma.pattern.strategynonextends.service.주유전략;
import com.acma.pattern.strategynonextends.service.impl.경유전략;
import com.acma.pattern.strategynonextends.service.impl.휘발유전략;
 
public class 전략패턴 {
    
    @Test
    public void 전략패턴테스트() {
        
        주유전략 주유전략변수 = null;
        자동차컨텍스트 승용차 = new 자동차컨텍스트();
        주유전략변수 = new 휘발유전략();
        승용차.set주유전략(주유전략변수);
        승용차.주유하다();
        
        승용차.set주유전략(new 경유전략());
        승용차.주유하다();
        
        
        자동차컨텍스트 SUV = new 자동차컨텍스트();
        주유전략변수 = new 경유전략();
        SUV.set주유전략(주유전략변수);
        SUV.주유하다();
        
        SUV.set주유전략(new 휘발유전략());
        SUV.주유하다();
    }
}
Colored by Color Scripter
 

 

6) 실행 결과

 

위의 예제보다는 좀 더 소스 확장에 자유로워진 것처럼 보인다.

지금은 자동차의 주유 전략에 대해서만 예제를 작성하였지만 주유가 아닌 자동차의 종류, 자동차가 다니는 길의 종류 등의 조건이 추가되더라도 소스 확장에 무리는 없어 보인다.

 

솔직히 지금 이렇게 열심히 예제를 만들면서 전략 패턴(Strategy Pattern)에 대해서 공부를 하고 있지만 여전히 내가 이해하고 있는 것이 잘못되진 않았는지에 대해서 걱정이다.

 

그래서 조금 더 공부한 후에 다른 예제로 한 번 더 포스팅을 하도록 하겠다. 

 

좀더 보완하자면 이렇게 되지 않을까 싶은 생각이 든다...

컨텍스트를 운송수단(자동차보다 상위 개념)으로 수정하여 전략을 추가하였다.

 


 

위의 경우 운송수단을 구체 클래스로 정의하였지만 여기서는 추상 클래스로 정의하고 운송수단을 세분화하여 상속받도록 수정하였다.

 

 

전략패턴다이어그램-1.pptx
0.07MB
다운로드

 

 

 

# 클라이언트가 전략을 생성하여 전략을 실행할 컨텍스트에게 주입하는 패턴이 전략 패턴이다.