1장. 헤드퍼스트 디자인 패턴 - 전략 패턴
안녕하세요 😀
유로띠 입니다 😉
헤드퍼스트 디자인 패턴
TIL (Today I Learned)
3줄 요약
✏️ 바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.
✏️ 상속보다는 구성을 활용한다.
✏️ 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
DAY 1
오늘 읽은 범위: 1장 디자인 패턴의 세계로 떠나기 - 디자인 패턴 소개와 전략 패턴
😉 책에서 기억하고 싶은 내용을 써보세요.
오리 시뮬레이션 게임, SimUduck
모든 오리가 꽥(quack) 소리를 내고 수영(swim)을 할 수 있기에 슈퍼클래스로 작성합니다.
오리마다 모양을 다르기 때문에 display는 오버라이드 합니다.
fly() 기능을 추가해 봅니다.
RubberDuck(고무 오리) 클래스도 추가되었습니다.
문제가 발생했습니다. 😥
Duck의 몇몇 서브클래스만 날아야 한다는 사실을 깜빡했습니다.
슈퍼클래스에 fly() 메소드를 추가한 결과, 날아다니면 안 되는 오리에게도 날아다니는 기능이 추가됐습니다.
코드의 일부분만 고쳤는데, 프로그램 전체에 고무 오리(RubberDuck)가 날아다니는 오류가 발생했습니다.
상속을 생각하기
quack() 메소드을 오리에 따라 오버라이드 했듯이 RubberDuck에 fly()를 오버라이드를 진행해 봅니다.
이렇게 상속하면 문제가 무엇일까요? 🧐
❌ 서브 클래스에서 코드가 중복된다.
❌ 모든 오리의 행동을 알기 힘들다.
❌ 실행 시에 특징을 바꾸기 힘들다.
❌ 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.
상속은 올바른 해결책이 아닙니다.
인터페이스를 설계합니다.
상속을 계속 활용한다면 규격이 바뀔 때마다 프로그램에 추가했던 Duck의 서브 클래스 fly()와 quack() 메소드를 일일이 살펴보고 상황에 따라 오버라이드해야 합니다. 그것도 영원히 반복해서 말이죠(p.42)
특정 형식의 오리만 날거나 꽥꽥거릴 수 있도록 하는 더 깔끔한 방법을 찾아야 합니다. (p.42)
Flyable과 Quackable 인터페이스를 만들어서 해당 기능을 원하는 오리에게만 넣어서 사용하기로 생각합니다.
하지만! 코드의 중복이 발생하고 날아가는 동작을 조금 바꾸기 위해 Duck의 서브클래스에서 날아다닐 수 있는 모든 코드를 전부 고쳐야 하는 상황이 발생됩니다. (p.43)
해결 방법 고민하기 🧐
서브클래스에서 Flyable, Quackable을 구현해서 일부 문제점은 해결할 수 있지만, 코드를 재사용하지 않으므로 코드 관리에 커다란 문제가 생깁니다.
물론 날 수 있는 오리 중에서도 날아다니는 방식이 서로 다를 수 있다는 문제도 포함해서 말이죠(p.43)
문제를 명확하게 파악하기 😏
상속이 그리 성공적인 해결책이 아니라는 사실은 이제 확실히 알고 있습니다. 서브클래스마다 오리의 행동이 바뀔 수 있는데도 모든 서브클래스에서 한 가지 행동만 사용하도록 하는 것은 그리 올바르지 못하니까요
인터페이스를 사용하는 방법은 괜찮아 보였지만, 자바 인터페이스에는 구현된 코드가 없으므로 코드를 재사용할 수 없다는 문제점이 있습니다. 즉 한 가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 서브클래스를 전부 찾아서 코드를 일일이 고쳐야 하고, 그 과정에서 새로운 버그가 생길 가능성도 있습니다. (p.45)
디자인 원칙
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화합니다.
그러면 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상시킬 수 있습니다. (p.45)
바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다. (p.45)
바뀌는 부분과 그렇지 않은 부분 분리하기
fly()와 quack()은 Duck 클래스에 있는 오리 종류에 따라 달라지는 부분입니다.
fly()와 quack()을 Duck 클래스로부터 분리하려면 2개의 메소드를 모두 Duck 클래스에서 끄집어내서 각 행동을 나타낼 클래스 집합을 새로 만들어야 합니다.
오리의 행동을 디자인하는 방법
각 행동은 인터페이스로 표현하고 이런 인터페이스를 사용해서 행동을 구현하겠습니다.
나는 행동과 꽥꽥거리는 행동은 이제 Duck클래스에서 구현하지 않습니다.
대신 특정 행동만을 목적으로 하는 클래스의 집합을 만들겠습니다.
행동(behavior) 인터페이스는 Duck클래스가 아니라 방금 설명한 행동 클래스에서 구현합니다.
이 방법은 지금까지 썼던 행동을 Duck 클래스에서 구체적으로 구현하거나 서브클래스 자체에서 별도로 구현하는 방법과는 상반된 방법입니다. 전에 썼던 방법은 항상 특정 구현에 의존했기에 행동을 변경할 여지가 없었죠.
새로운 디자인을 사용하면 Duck 서브클래스는 인터페이스로 표현되는 행동을 사용합니다.
따라서 실제 행동 구현은 Duck 서브클래스에 국한되지 않습니다. (p.47)
디자인 원칙
구현보다는 인터페이스에 맞춰서 프로그래밍한다.
인터페이스에 맞춰서 프로그래밍한다라는 말은 사실 상위 형식에 맞춰서 프로그래밍한다라는 말입니다.
즉, 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다.
객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다.
그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
라는 뜻으로 생각하면 됩니다. (p.48)
오리의 행동을 구현하는 방법
날 수 있는 클래스는 무조건 FlyBehavior 인터페이스를 구현합니다.
날 수 있는 클래스를 새로 만들 때는 무조건 fly 메소드를 구현해야 합니다.
꽥꽥 거리는 것과 관련된 행동도 마찬가지입니다. 반드시 구현해야만 하는 quack() 메소드가 들어있는 QuackBehavior 인터페이스가 있습니다.
이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있습니다. 그런 행동이 더 이상 Duck 클래스 안에 숨겨져 있지 않으니까요.
그리고 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck 클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수 있습니다. (p.49)
오리 행동 통합하기
가장 중요한 점은 나는 행동과 꽥꽥거리는 행동을 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임한다는 것입니다.
우선 Duck 클래스에 flyBehavior와 quackBehavior라는 인터페이스 형식의 인스턴스 변수를 추가합니다.
각 오리 객체에서는 실행 시에 이 변수에 특정 행동 형식의 레퍼런스를 다형적으로 설정합니다.
나는 행동과 꽥꽥거리는 행동은 flyBehavior와 quackBehavior 인터페이스로 옮겨놨으므로 Duck 클래스와 모든 서브 클래스에서 fly()와 quack()메소드를 제거합니다.
Duck클래스에 fly()와 quack() 대신 performFly()와 performQuack()이라는 메소드를 넣습니다.
🦆 오리(Duck) 코드
🟢 Duck.java
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("모든 오리는 물에 뜹니다. 가짜오리도 뜨죠.");
}
}
🟢 인터페이스 - FlyBehavior.java
package strategyPattern;
public interface FlyBehavior {
void fly();
}
🟢 행동 구현 클래스 - FlyWithWings.java / FlyNoWay.java
package strategyPattern;
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("날고 있어요!");
}
}
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
System.out.println("저는 못 날아요!");
}
}
🟢 QuackBehavior.java
package strategyPattern;
public interface QueakBehavior {
void quack();
}
🟢 행동 구현 클래스 - Quack.java / MuteQuack.java / Squeak.java
package strategyPattern;
public class Quack implements QueakBehavior {
@Override
public void quack() {
System.out.println("꽥꽥!!");
}
}
public class MuteQuack implements QueakBehavior {
@Override
public void quack() {
System.out.println("조용");
}
}
public class Squeak implements QueakBehavior {
@Override
public void quack() {
System.out.println("삑");
}
}
🟢 MallardDuck.java
package strategyPattern;
public class MallardDuck extends Duck {
public MallardDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("저는 물오리 입니다.");
}
}
🟢 main.java
MallardDuck에서 상속받은 performQuack() 메소드가 호출됩니다.
이 메소드에서는 객체의 QuackBehavior에게 할 일을 위임합니다.
package strategyPattern;
public class DuckApplication {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
}
}
// console
날고 있어요!.
꽥!!
동적으로 행동 지정하기
setter method를 호출하는 방법으로 동적(전략적)으로 행동하도록 변경해 봅니다.
Duck 클래스에 setFlyBehavior와 setQuackBehavior 메소드 2개를 추가합니다.
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;
}
}
mallardDuck에 fly 기능을 전략적으로 변경해 봅니다. 🙌
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
날고 있어요!.
꽥!!
저는 못 날아요.
디자인 패턴 : 전략 패턴
전략 패턴(strategy Pattern)은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해 줍니다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.
캡슐화된 행동 살펴보기
클라이언트에서는 나는 행동과 꽥꽥거리는 행동을 캡슐화된 알고리즘으로 구현합니다.
각 오리에는 FlyBehavior와 QuackBehavior가 있으며, 각각 나는 행동과 꽥꽥거리는 행동을 위임받습니다.
이런 식으로 두 클래스를 합치는 것을 구성(composition)을 이용한다라고 부릅니다.
여기에 나와 있는 오리 클래스에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받습니다. (p.58)
디자인 원칙
상속보다는 구성을 활용한다.
🧐 오늘 익은 소감은? 떠오르는 생각을 가볍게 적어보세요
✅ 전략 패턴 안에 OCP(개방 폐쇄 원칙)과 DIP(의존 역전 원칙)을 적용하고 있다. 중요한 점은 바뀌는 부분과 그렇지 않은 부분을 나누고 바뀌는 행동을 캡슐화하여 변경에 유연성을 제공하는 것 같다.
✅ 실무에서 이렇게 전략 패턴을 사용하도록 기존 기능이 변경되거나 하는 부분이 있는지 확인해보고 반영했으면 좋겠다.
🙋♂️ 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
⭐️