공부하게 된 배경
디자인 패턴은 주니어 개발자에서 나아가기 위한 필수 코스라고 생각한다.
디자인 패턴이 왜 중요할까? 왜 수많은 기업들에서는 디자인 패턴을 적용해서 개발하려하고, 개발자들에게 필독 도서로 추천할까?
아마도 그 이유는 결국 ‘유지보수’로 귀결되겠지만, 그 이유를 제대로 알아보고 필요성을 느껴보려 이 책을 공부하려 한다.
디자인패턴을 다룰 때의 장점은 다음과 같다.
- 공통으로 아는 용어를 쓰기에 간단한 단어로 많은 얘기를 할 수 있다.
- 객체와 클래스 구현에 대해 시간을 버릴 필요가 없어 디자인에 포커싱할 수 있다.
- 전문용어를 통해 명확한 내용전달로 팀의 능력이 극대화될 수 있다.
디자인패턴은 코드가 아닌 경험을 재사용하는 것이다.
수많은 선배 개발자가 우리와 똑같은 문제를 경험하고 해결하면서 익혔던 지혜와 교훈을 살펴 활용하는 법을 배워보도록 하겠다.
다루는 내용
- 디자인 패턴의 활용 분야
- 전략 패턴
Chapter1. 디자인 패턴의 세계로 떠나기
: 디자인 패턴 소개와 전략 패턴
학습 목표
- 디자인 패턴의 활용 분야와 디자인 패턴으로 얻을 수 있는 장점을 알아보기.
- 몇 가지 핵심적인 객체지향 디자인 원칙을 살펴본 후 한가지 패턴을 정해 디자인 원칙이 어떤식으로 작동하는 지 알아보기. (전략 패턴)
디자인 원칙
첫번째 디자인 원칙
💡 애플리케이션에서 새로운 요구사항이 있을 때 마다 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
즉, 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화한다. 이렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.
위 개념은 매우 간단하지만 다른 모든 디자인 패턴의 기반을 이루는 원칙이다.
결국, 모든 패턴은 ‘시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는’ 방법들을 제공한다.
두번째 디자인 원칙
💡 구현보다는 인터페이스에 맞춰서 프로그래밍 한다. = 상위 형식에 맞춰서 프로그래밍한다
실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록, 상위 형식(SuperType)에 맞춰 프로그래밍해서 다형성을 활용해야 한다.
- 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다.
- 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다.
- 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
세번째 디자인 원칙
💡 구성(composition)을 이용한다 = 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.
서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일할 때 완전히 결정되며,
모든 서브클래스에서 똑같은 행동을 상속받아야 한다. (즉, 유연성이 떨어진다)
하지만 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있게 된다.
- 객체를 동적으로 구성하면 기존 코드를 고치는 대신 새로운 코드를 만들어서 기능을 추가할 수 있다.
- 즉, 기존 코드를 건드리지 않으므로 코드 수정에 따른 버그나 의도하지 않은 부작용을 원천봉쇄할 수 있다.
전략 패턴 (Strategy Pattern)
💡 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴
전략 패턴은 특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하며 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다. (독립적으로 변경 가능)
간단하게 정리하면, 변경 가능성이 큰 부분을 분리하고, 인터페이스를 이용하여 구현체로 캡슐화하여 실행 중 해당 구현체만 setter 등의 메서드를 통해 상호 교체 가능하게 만드는 것이다.
예시 상황
- ‘SimUDuck’ 이라는 오리 시뮬레이션 게임이 있다.
- 이 게임에는 다양한 오리가 등장한다.
- 현재 이 게임은, Duck 이라는 슈퍼 클래스와 이 클래스를 확장한 다양한 종류의 오리 클래스들로 구성되어 있다.
- 모든 오리가 꽥(quack) 소리를 내고 수영(swim)을 할 수 있기에 슈퍼클래스에 작성하고, 모양은 오리마다 다르기 때문에 display는 오버라이드한다.
여기서, 오리가 하늘을 나는 기능을 추가해야 합니다.
해결책 1) 상속
상위 클래스인 Duck Class에 fly() 메서드를 추가합니다.
문제 발생
몇몇 서브클래스의 오리들만 날아야 하는데 모든 클래스의 오리들이 날고 있다
RubberDuck의 fly 함수를 재정의(Override)해 고무 오리의 fly()는 아무 행동도 하지 않도록 ‘일단’ 수정을 하였습니다.
이렇게 상속하면 생기는 문제
- 서브 클래스에서 코드가 중복된다.
- 모든 오리의 행동을 알기 힘들다.
- 실행 시에 특징을 바꾸기 힘들다.
- 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.
상속을 계속 활용한다면 규격이 바뀔 때마다 프로그램에 추가했던 Duck의 서브 클래스 fly()와 quack() 메소드를 일일이 살펴보고 상황에 따라 오버라이드해야 합니다. 그것도 영원히 반복해서 말이죠(p.42)
특정 형식의 오리만 날거나 꽥꽥거릴 수 있도록 하는 더 깔끔한 방법을 찾아야 합니다. (p.42)
해결책 2) 인터페이스
Flyable과 Quackable 인터페이스를 만들어서 해당 기능을 원하는 오리에게만 넣어서 사용하기 합니다.
하지만! 코드의 중복이 발생하고 날아가는 동작을 조금 바꾸기 위해 Duck의 서브클래스에서 날아다닐 수 있는 모든 코드를 전부 고쳐야 하는 상황이 발생됩니다. (p.43)
서브클래스에서 Flyable, Quackable을 구현해서 일부 문제점은 해결할 수 있지만, 코드를 재사용하지 않으므로 코드 관리에 커다란 문제가 생깁니다.
물론 날 수 있는 오리 중에서도 날아다니는 방식이 서로 다를 수 있다는 문제도 포함해서 말이죠(p.43)
인터페이스를 사용하면 생기는 문제
- 코드를 재사용할 수 없다.
- 즉, 한가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 서브클래스를 전부 찾아서 코드를 일일이 고쳐야한다.
- 그 과정에서 새로운 버그가 생길 가능성이 있다.
=> 첫번째 디자인 원칙을 생각해보자.
해결책 3) 전략 패턴 사용
(첫번째 디자인 원칙) 바뀌는 부분과 그렇지 않은 부분 분리하기
fly()와 quack()은 Duck 클래스에 있는 오리 종류에 따라 달라지는 부분입니다.
fly()와 quack()을 Duck 클래스로부터 분리하려면 2개의 메소드를 모두 Duck 클래스에서 끄집어내서 각 행동을 나타낼 클래스 집합을 새로 만들어야 합니다.
(두번째 디자인 원칙)구현보다는 인터페이스에 맞춰서 프로그래밍한다.
: 인터페이스에 맞춰서 프로그래밍한다라는 말은 사실 상위 형식에 맞춰서 프로그래밍한다라는 말
즉, 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다.
객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다. 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
따라서 이제 나는 행동과 꽥꽥거리는 행동은 이제 Duck클래스에서 구현하지 않습니다.
대신 특정 행동만을 목적으로 하는 클래스의 집합을 만들겠습니다.
행동(behavior) 인터페이스는 Duck클래스가 아니라 방금 설명한 행동 클래스에서 구현합니다.
이 방법은 지금까지 썼던 행동을 Duck 클래스에서 구체적으로 구현하거나 서브클래스 자체에서 별도로 구현하는 방법과는 상반된 방법입니다. 전에 썼던 방법은 항상 특정 구현에 의존했기에 행동을 변경할 여지가 없었죠.
새로운 디자인을 사용하면 Duck 서브클래스는 인터페이스로 표현되는 행동을 사용합니다.
따라서 실제 행동 구현은 Duck 서브클래스에 국한되지 않습니다. (p.47)
날 수 있는 클래스는 무조건 FlyBehavior 인터페이스를 구현합니다. 날 수 있는 클래스를 새로 만들 때는 무조건 fly 메소드를 구현해야 합니다.
꽥꽥 거리는 것과 관련된 행동도 마찬가지입니다. 반드시 구현해야만 하는 quack() 메소드가 들어있는 QuackBehavior 인터페이스가 있습니다.
이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있습니다. 그런 행동이 더 이상 Duck 클래스 안에 숨겨져 있지 않으니까요.
그리고 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck 클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수 있습니다. (p.49)
(세번째 디자인 원칙) 동적으로 행동 지정하기
setter method를 호출하는 방법으로 동적(전략적)으로 행동하도록 변경해 봅니다.
Duck 클래스에 setFlyBehavior와 setQuackBehavior 메소드 2개를 추가합니다.
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
package strategyPattern;
public abstract class Duck {
public FlyBehavior flyBehavior;
public QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.printLn("모든 오리는 물에 뜹니다. 가짜오리도 뜨죠.");
}
public void setFlyBehavior(FlyBehavior fly) {
flyBehavior = fly;
}
public void setQuackBehavior(QuackBehavior quack) {
quackBehavior = quack;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package strategyPattern;
public class DuckApplication {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
mallard.setFlyBehavior(new FlyNoWay());
mallard.performFly();
}
}
//console
날고 있어요!.
꽥!!
저는 못 날아요.
클라이언트에서는 나는 행동과 꽥꽥거리는 행동을 캡슐화된 알고리즘으로 구현합니다.
각 오리에는 FlyBehavior와 QuackBehavior가 있으며, 각각 나는 행동과 꽥꽥거리는 행동을 위임받습니다.
이런 식으로 두 클래스를 합치는 것을 구성(composition)을 이용한다라고 부릅니다.
여기에 나와 있는 오리 클래스에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받습니다. (p.58)
전략 패턴은 항상 옳을까?
장점
새로운 전략이 추가, 수정되어도 컨텍스트의 코드 변경 없이도 새로운 전략을 추가, 수정할 수 있으므로 코드의 변경에 매우 유연하다.
즉, OCP 원칙을 지킬 수 있다.
단점
코드의 복잡성이 증가한다.
즉, 전략의 변경 여지가 없고, 전략의 개수가 하나 혹은 두개 정도일 때는 전략 패턴의 사용이 오히려 코드만 복잡하게 만들 수도 있다.
언제 사용하여야 할까?
변경 여지가 없을 때는 고려하지 않아도 될까? 변경의 여지가 없다고 해서 (전략 패턴을 사용하는 것이 아닌) 구현체 만을 사용해서 개발하게 되면 추후 큰 변경이나 추가,수정이 있을 때 어려워질 수 있다.
전략 패턴은 추상화 기법이다. 추상화를 통한 이점을 충분히 파악하고 사용을 고려하는 자세를 갖는 것이 중요하다.